TransmittableThreadLocal在使用线程池等会缓存线程的组件情况下传递ThreadLocal

Easter79
• 阅读 877

1、简介

TransmittableThreadLocal 是Alibaba开源的、用于解决 “在使用线程池等会缓存线程的组件情况下传递ThreadLocal” 问题的 InheritableThreadLocal 扩展。若希望 TransmittableThreadLocal 在线程池与主线程间传递,需配合 TtlRunnableTtlCallable 使用。

2、使用场景

下面是几个典型场景例子。

  1. 分布式跟踪系统
  2. 应用容器或上层框架跨应用代码给下层SDK传递信息
  3. 日志收集记录系统上下文

3、简单分析使用

JDKInheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时ThreadLocal值传递到 任务执行时

下面分析下[InheritableThreadLoc](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fdocs.oracle.com%2Fjavase%2F10%2Fdocs%2Fapi%2Fjava%2Flang%2FInheritableThreadLocal.html)InheritableThreadLocal类重写了ThreadLocal的3个函数:

/**
  * 该函数在父线程创建子线程,向子线程复制InheritableThreadLocal变量时使用
*/
protected T childValue(T parentValue) {
    return parentValue;
}

/**
  * 由于重写了getMap,操作InheritableThreadLocal时,
  * 将只影响Thread类中的inheritableThreadLocals变量,
  * 与threadLocals变量不再有关系
*/
ThreadLocalMap getMap(Thread t) {
     return t.inheritableThreadLocals;
}

/**
 * 类似于getMap,操作InheritableThreadLocal时,
 * 将只影响Thread类中的inheritableThreadLocals变量,
 * 与threadLocals变量不再有关系
 */
void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

注意:重写了getMap()和createMap()两个函数,说到InheritableThreadLocal,还要从Thread类说起:

public class Thread implements Runnable {
   ......(其他源码)
    /* 
     * 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,
     * 主要用于父子线程间ThreadLocal变量的传递
     * 本文主要讨论的就是这个ThreadLocalMap
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ......(其他源码)
}

Thread类中包含 threadLocalsinheritableThreadLocals 两个变量,其中 inheritableThreadLocals 即主要存储可自动向子线程中传递的ThreadLocal.ThreadLocalMap。
接下来看一下父线程创建子线程的流程,我们从最简单的方式说起:

用户创建Thread

hread thread = new Thread();

**
 * Allocates a new {@code Thread} object. This constructor has the same
 * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
 * {@code (null, null, gname)}, where {@code gname} is a newly generated
 * name. Automatically generated names are of the form
 * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
 */
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

Thread初始化

/**
 * 默认情况下,设置inheritThreadLocals可传递
 */
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}

/**
 * 初始化一个线程.
 * 此函数有两处调用,
 * 1、上面的 init(),不传AccessControlContext,inheritThreadLocals=true
 * 2、传递AccessControlContext,inheritThreadLocals=false
 */
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    ......(其他代码)

    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

    ......(其他代码)
}

可以看到,采用默认方式产生子线程时,inheritThreadLocals=true;若此时父线程inheritableThreadLocals不为空,则将父线程inheritableThreadLocals传递至子线程。

让我们继续追踪createInheritedMap

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}


/**
 * 构建一个包含所有parentMap中Inheritable ThreadLocals的ThreadLocalMap
 * 该函数只被 createInheritedMap() 调用.
 */
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    // ThreadLocalMap 使用 Entry[] table 存储ThreadLocal
    table = new Entry[len];

    // 逐一复制 parentMap 的记录
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                // 可能会有同学好奇此处为何使用childValue,而不是直接赋值,
                // 毕竟childValue内部也是直接将e.value返回;
                // 个人理解,主要为了减轻阅读代码的难度
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

从ThreadLocalMap可知,子线程将parentMap中的所有记录逐一复制至自身线程。InheritableThreadLocal主要用于子线程创建时,需要自动继承父线程的ThreadLocal变量,方便必要信息的进一步传递。

接下来提供的TransmittableThreadLocal类继承并加强InheritableThreadLocal类,解决上述的问题。

使用类TransmittableThreadLocal来保存值,并跨线程池传递。

TransmittableThreadLocal继承InheritableThreadLocal,使用方式也类似。

相比InheritableThreadLocal,添加了

  1. protected方法copy
    用于定制 任务提交给线程池时 的ThreadLocal值传递到 任务执行时 的拷贝行为,缺省传递的是引用。
  2. protected方法beforeExecute/afterExecute
    执行任务(Runnable/Callable)的前/后的生命周期回调,缺省是空操作。

1. 简单使用

父线程给子线程传递值。

示例代码:

// 在父线程中设置 TransmittableThreadLocal parent = new TransmittableThreadLocal(); parent.set("value-set-in-parent");

// =====================================================

// 在子线程中可以读取,值是"value-set-in-parent" String value = parent.get();

这是其实是InheritableThreadLocal的功能,应该使用InheritableThreadLocal来完成。

但对于使用线程池等会池化复用线程的组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时ThreadLocal值传递到 任务执行时

解决方法参见下面的这几种用法。

2. 保证线程池中传递值

2.1 修饰RunnableCallable

使用TtlRunnableTtlCallable来修饰传入线程池的RunnableCallable

示例代码:

TransmittableThreadLocal parent = new TransmittableThreadLocal(); parent.set("value-set-in-parent");

Runnable task = new Task("1"); // 额外的处理,生成修饰了的对象ttlRunnable Runnable ttlRunnable = TtlRunnable.get(task); executorService.submit(ttlRunnable);

// =====================================================

// Task中可以读取,值是"value-set-in-parent" String value = parent.get();

上面演示了RunnableCallable的处理类似

TransmittableThreadLocal parent = new TransmittableThreadLocal(); parent.set("value-set-in-parent");

Callable call = new Call("1"); // 额外的处理,生成修饰了的对象ttlCallable Callable ttlCallable = TtlCallable.get(call); executorService.submit(ttlCallable);

// =====================================================

// Call中可以读取,值是"value-set-in-parent" String value = parent.get();

整个过程的完整时序图

TransmittableThreadLocal在使用线程池等会缓存线程的组件情况下传递ThreadLocal

2.2 修饰线程池

省去每次RunnableCallable传入线程池时的修饰,这个逻辑可以在线程池中完成。

通过工具类com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法:

  • getTtlExecutor:修饰接口Executor
  • getTtlExecutorService:修饰接口ExecutorService
  • getTtlScheduledExecutorService:修饰接口ScheduledExecutorService

示例代码:

ExecutorService executorService = ... // 额外的处理,生成修饰了的对象executorService executorService = TtlExecutors.getTtlExecutorService(executorService);

TransmittableThreadLocal parent = new TransmittableThreadLocal(); parent.set("value-set-in-parent");

Runnable task = new Task("1"); Callable call = new Call("2"); executorService.submit(task); executorService.submit(call);

// =====================================================

// Task或是Call中可以读取,值是"value-set-in-parent" String value = parent.get();

2.3 使用Java Agent来修饰JDK线程池实现类

这种方式,实现线程池的传递是透明的,代码中没有修饰Runnable或是线程池的代码。即可以做到应用代码 无侵入
# 关于 无侵入 的更多说明参见文档Java Agent方式对应用代码无侵入

示例代码:

// ## 1. 框架上层逻辑,后续流程框架调用业务 ## TransmittableThreadLocal context = new TransmittableThreadLocal(); context.set("value-set-in-parent");

// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ## ExecutorService executorService = Executors.newFixedThreadPool(3);

Runnable task = new Task("1"); Callable call = new Call("2"); executorService.submit(task); executorService.submit(call);

// ## 3. 框架下层逻辑 ## // Task或是Call中可以读取,值是"value-set-in-parent" String value = context.get();

Demo参见AgentDemo.kt。执行工程下的脚本scripts/run-agent-demo.sh即可运行Demo。

目前TTL Agent中,修饰了JDK中的线程池实现如下:

  1. java.util.concurrent.ThreadPoolExecutor 和 java.util.concurrent.ScheduledThreadPoolExecutor
    修饰实现代码在TtlExecutorTransformlet.java
  2. java.util.concurrent.ForkJoinTask(对应的线程池组件是java.util.concurrent.ForkJoinPool
    修饰实现代码在TtlForkJoinTransformlet.java
  3. java.util.TimerTask的子类(对应的线程池组件是java.util.Timer
    修饰实现代码在TtlTimerTaskTransformlet.java
    **注意**:缺省没有开启TimerTask的修饰,使用Agent参数ttl.agent.enable.timer.task开启:-javaagent:path/to/transmittable-thread-local-2.x.x.jar=ttl.agent.enable.timer.task:true
    更多关于TTL Agent参数的配置说明详见TtlAgent.java的JavaDoc

关于java.util.TimerTask/java.util.Timer

TimerJDK 1.3的老类,不推荐使用Timer类。

推荐用ScheduledExecutorService
ScheduledThreadPoolExecutor实现更强壮,并且功能更丰富。 如支持配置线程池的大小(Timer只有一个线程);TimerRunnable中抛出异常会中止定时执行。更多说明参见10. Mandatory Run multiple TimeTask by using ScheduledExecutorService rather than Timer because Timer will kill all running threads in case of failing to catch exceptions. - Alibaba Java Coding Guidelines

关于boot class path设置

因为修饰了JDK的标准库的类,标准库由bootstrap class loader加载;上面修饰后的JDK类引用了TTL的代码,所以TTLJar需要加到boot class path上。

TTLv2.6.0开始,加载TTL Agent会自动把自己的Jar设置到boot class path上。

**注意**:不能修改从Maven库下载的TTLJar的文件名(形如transmittable-thread-local-2.x.x.jar)。 如果修改了,则需要自己手动通过-Xbootclasspath JVM参数来显式配置(就像TTL之前的版本的做法一样)。

实现是通过指定TTL Java Agent Jar文件里manifest文件(META-INF/MANIFEST.MF)的Boot-Class-Path属性:

Boot-Class-Path

A list of paths to be searched by the bootstrap class loader. Paths represent directories or libraries (commonly referred to as JAR or zip libraries on many platforms). These paths are searched by the bootstrap class loader after the platform specific mechanisms of locating a class have failed. Paths are searched in the order listed.

Java的启动参数配置

Java的启动参数加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar

如果修改了下载的TTLJar的文件名(transmittable-thread-local-2.x.x.jar),则需要自己手动通过-Xbootclasspath JVM参数来显式配置:
比如修改文件名成ttl-foo-name-changed.jar,则还加上Java的启动参数:-Xbootclasspath/a:path/to/ttl-foo-name-changed.jar

Java命令行示例如下:

java -javaagent:path/to/transmittable-thread-local-2.x.x.jar \ -cp classes \ com.alibaba.ttl.threadpool.agent.demo.AgentDemo

或是

java -javaagent:path/to/ttl-foo-name-changed.jar \ -Xbootclasspath/a:path/to/ttl-foo-name-changed.jar \ -cp classes \ com.alibaba.ttl.threadpool.agent.demo.AgentDemo

Maven依赖

com.alibaba transmittable-thread-local 2.10.2
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
2年前
java各种面试问题
二、Java多线程相关线程池的原理,为什么要创建线程池?创建线程池的方式;线程的生命周期,什么时候会出现僵死进程;说说线程安全问题,什么实现线程安全,如何实现线程安全;创建线程池有哪几个核心参数?如何合理配置线程池的大小?volatile、ThreadLocal的使用场景和原理;
Wesley13 Wesley13
2年前
4、jstack查看线程栈信息
1、介绍利用jps、top、jstack命令找到进程中耗时最大的线程,以及线程状态等等,同时最后还可以显示出死锁的线程查找:FoundoneJavaleveldeadlock即可1、jps获得进程号!(https://oscimg.oschina.net/oscnet/da00a309fa6
Wesley13 Wesley13
2年前
Java ThreadLocal的内存泄漏问题
ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:\存储单个线程上下文信息。比如存储id等;\使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;\减少参数传递。比如做一个trace工具,能够输出工程从开始到结
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
2年前
Noark入门之线程模型
0x00单线程多进程单线程与单进程多线程的目的都是想尽可能的利用CPU,减少CPU的空闲时间,特别是多核环境,今天咱不做深度解读,跳过...0x01线程池锁最早的一部分游戏服务器是采用线程池的方式来处理玩家的业务请求,以达最大限度的利用多核优势来提高处理业务能力。但线程池同时也带来了并发问题,为了解决同一玩家多个业务请求不被
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k