# 5.3 线程关联
# 5.3.1 线程上下文丢失
ThreadLocal能够很好的解决线程内部的上下文传递问题,但是对于使用多线程的异步场景,线程上下文会丢失。 下面的代码,在主线程中设置线程变量,然后启动一个子线程,在子线程中获取线程变量的值。
public class ThreadLocalDemo {
public static ThreadLocal<Integer> context = new ThreadLocal<>();
public static void main(String[] args) {
// 设置线程变量的值 (main线程)
context.set(1000);
// 从线程变量中取出值
Integer ctx = context.get();
System.out.println("ctx= " + ctx);
// thread线程是main线程创建的子线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Integer ctx = context.get();
System.out.println("ctx= " + ctx);
}
});
thread.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
输出结果如下,可以看到子线程无法获取主线程设置的线程变量。
ctx= 1000
ctx= null
2
从线程变量的名称和作用来看,这个子线程获取为空是符合预期的,但是从线程上下文传递的功能角度来看,却是不满足需求的。 于是Java官方又提供了ThreadLocal的子类InheritableThreadLocal来解决创建新线程时的上下文传递丢失的问题。
# 5.3.2 InheritableThreadLocal
使用TheadLocal时,子线程访问不了父线程的本地变量,InheritableThreadLocal很好的解决了该问题。 InheritableThreadLocal源码如下。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 接收父线程本地变量的值
// 这个方法在父线程创建子线程时调用
protected T childValue(T parentValue) {
// 这里是直接返回原值
return parentValue;
}
// 使用inheritableThreadLocals保存线程变量
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
// 初始化inheritableThreadLocals
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
不同于ThreadLocal,在使用InheritableThreadLocal对象时,变量保存在inheritableThreadLocals中。 下面是Thread类中两个变量的定义。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
2
再来看下在线程创建时如何实现线程变量的copy过程。
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
this.name = name;
Thread parent = currentThread();
// 安全、校验等代码省略...
// 线程常规的初始化动作
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.target = target;
setPriority(priority);
// 线程变量的map拷贝
if (inheritThreadLocals && parent.inheritableThreadLocals != null){
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
this.stackSize = stackSize;
this.tid = nextThreadID();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
线程变量的map拷贝在ThreadLocal.createInheritedMap中,实际是创建一个新的map并将值复制一份。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 遍历父线程的table
for (Entry e : parentTable) {
if (e != null) {
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 赋值
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++;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5.3.3 transmittable-thread-local
JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。 对于线程池场景,线程由线程池创建好,并且线程是池化起来反复使用的, 这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的ThreadLocal值传递到任务执行时。
TransmittableThreadLocal(TTL) 是阿里巴巴开源的项目,在使用线程池等会池化复用线程的执行组件情况下, 提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
TransmittableThreadLocal继承InheritableThreadLocal,使用方式也类似。 相比InheritableThreadLocal,添加了protected的transmitteeValue()方法, 用于定制任务提交给线程池时的ThreadLocal值传递到任务执行时的传递方式。
# 5.3.3.1 简单使用
- 父线程给子线程传递值
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
// =====================================================
// 在子线程中可以读取,值是"value-set-in-parent"
String value = context.get();
2
3
4
5
6
7
8
9
10
11
这其实是InheritableThreadLocal的功能,可以使用InheritableThreadLocal来完成。
# 5.3.3.2 线程池中传递值
- 修饰Runnable和Callable
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// =====================================================
// Task中可以读取,值是"value-set-in-parent"
String value = context.get();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面演示了Runnable,Callable的处理类似。
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Callable call = new CallableTask();
// 额外的处理,生成修饰了的对象ttlCallable
Callable ttlCallable = TtlCallable.get(call);
executorService.submit(ttlCallable);
// =====================================================
// Call中可以读取,值是"value-set-in-parent"
String value = context.get();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 修饰线程池
省去每次Runnable和Callable传入线程池时的修饰,这个逻辑可以在线程池中完成。 例子如下:
ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// =====================================================
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 使用Java Agent来修饰JDK线程池实现类
相比于SDK方式,这种方式,实现线程池上下文的传递是透明的,业务代码中没有修饰Runnable或是线程池的代码,即可以做到应用代码无侵入。
使用需要在应用启动参数中增加一个premain的agent,在应用启动之前修改线程的字节码,接入方式如下:
java -javaagent:path/to/transmittable-thread-local-2.x.y.jar springboot-application.jar
需要注意的是,如果有多个JavaAgent,需要将transmittable的Agent参数放到其他Agent参数之前。