目录
  1. 1. 增强版的ThreadLocal—-TransmittableThreadLocal
    1. 1.1. TransmittableThreadLocal解决线程变量继承问题
    2. 1.2. TransmittableThreadLocal实现原理
增强版的ThreadLocal----TransmittableThreadLocal

增强版的ThreadLocal—-TransmittableThreadLocal

  ThreadLocal是JDK里面提供的一个thread-local(线程局部)的变量,当一个变量被声明为ThreadLocal时候,每个线程会持有该变量的一个独有副本;但是ThreadLocal不支持继承性,虽然JDK里面提供了InheritableThreadLocal来解决继承性问题,但是其也是不彻底的,本节我们谈谈增强的TransmittableThreadLocal,其可以很好解决线程池情况下继承问题
  github地址为:https://github.com/alibaba/transmittable-thread-local,后面我们会探讨其内部实现原理

TransmittableThreadLocal解决线程变量继承问题

当一个变量被声明为ThreadLocal时候,每个线程会持有该变量的一个独有副本,比如下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private static ThreadLocal<String> parent = new ThreadLocal<String>();

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
// 设置本线程变量
parent.set(Thread.currentThread().getName() + "hello,jiaduo");
// dosomething
Thread.sleep(3000);
// 使用线程变量
System.out.println(Thread.currentThread().getName() + ":" + parent.get());
// 清除
parent.remove();

// do other thing
//.....
} catch (Exception e) {
e.printStackTrace();
}
}, "thread-1").start();

new Thread(() -> {
try {
// 设置本线程变量
parent.set(Thread.currentThread().getName() + "hello,jiaduo");
// dosomething
Thread.sleep(3000);
// 使用线程变量
System.out.println(Thread.currentThread().getName() + ":" + parent.get());
// 清除
parent.remove();

// do other thing
//.....

} catch (Exception e) {
e.printStackTrace();
}
}, "thread-2").start();
}

如上代码线程1和线程2各自持有parent变量中的副本,其相互之间并发访问自己的副本变量,不会存在线程安全问题

但是ThreadLocal不支持继承性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> parent = new ThreadLocal<String>();

parent.set(Thread.currentThread().getName() + "hello,jiaduo");

new Thread(() -> {
try {

// 使用线程变量
System.out.println(Thread.currentThread().getName() + ":" + parent.get());
// do other thing
// .....
} catch (Exception e) {
e.printStackTrace();
}
}, "child-thread").start();
}

如上代码main线程内设置了线程变量,然后在main线程内开启了子线程child-thread,然后在子线程内访问了线程变量,运行会输出:child-thread:null;也就是子线程访问不了父线程设置的线程变量;JDK中InheritableThreadLocal可以解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
InheritableThreadLocal<String> parent = new InheritableThreadLocal<String>();
parent.set(Thread.currentThread().getName() + "hello,jiaduo");

new Thread(() -> {
try {
// 使用线程变量
System.out.println(Thread.currentThread().getName() + ":" + parent.get());
// do other thing
// .....
} catch (Exception e) {
e.printStackTrace();
}
}, "child-thread").start();
}

运行代码会输出:child-thread:mainhello,jiaduo,可知InheritableThreadLocal支持继承性.但是InheritableThreadLocal的继承性是在new Thread创建子线程时候在构造函数内把父线程内线程变量拷贝到子线程内部的(可以参考《Java并发编程之美》一书),而线上环境我们很少亲自new线程,而是使用线程池来达到线程复用,线上环境一般是把异步任务投递到线程池内执行;所以父线程向线程池内投递任务时候,可能线程池内线程已经创建完毕了,所以InheritableThreadLocal就起不到作用了,例如下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 0.创建线程池
private static final ThreadPoolExecutor bizPoolExecutor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1));
public static void main(String[] args) throws InterruptedException {
// 1 创建线程变量
InheritableThreadLocal<String> parent = new InheritableThreadLocal<String>();
// 2 投递三个任务
for (int i = 0; i < 3; ++i) {
bizPoolExecutor.execute(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 3休眠4s
Thread.sleep(4000);
// 4.设置线程变量
parent.set("value-set-in-parent");
// 5. 提交任务到线程池
bizPoolExecutor.execute(() -> {
try {
// 5.1访问线程变量
System.out.println("parent:" + parent.get());
} catch (Exception e) {
e.printStackTrace();
}
});
}

如上代码2向线程池投递3任务,这时候线程池内2个核心线程会被创建,并且队列里面有1个元素.然后代码3休眠4s,旨在让线程池避免饱和执行拒绝策略,然后代码4设置线程变量,代码5提交任务到线程池.运行输出:parent:null,可知子线程内访问不到父线程设置变量

下面我们使用TransmittableThreadLocal修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 0.创建线程池
private static final ThreadPoolExecutor bizPoolExecutor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1));

public static void main(String[] args) throws InterruptedException {
// 1 创建线程变量
TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
// 2 投递三个任务
for (int i = 0; i < 3; ++i) {
bizPoolExecutor.execute(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 3休眠4s
Thread.sleep(4000);
// 4.设置线程变量
parent.set("value-set-in-parent");
// 5. 提交任务到线程池
Runnable task = () -> {
try {
// 5.1访问线程变量
System.out.println("parent:" + parent.get());
} catch (Exception e) {
e.printStackTrace();
}
};

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

bizPoolExecutor.execute(ttlRunnable);
}

如上代码5我们把具体任务使用TtlRunnable.get(task)包装了下,然后在提交到线程池,运行代码,输出:parent:value-set-in-parent,可知子线程访问到了父线程的线程变量

TransmittableThreadLocal实现原理

  鉴于InheritableThreadLocal已经实现了new Thread情况下的线程变量继承问题,所以TransmittableThreadLocal可以直接继承它,来继承该功能.其他我们需要做的就是如何在任务提交到线程池后,线程池线程运行任务时候任务内可以从线程变量里面获取到父线程设置的值
  InheritableThreadLocal的实现是在new Thread时候把父线程中的inheritableThreadLocals变量复制到了子线程,从而实现线程变量的继承特性.而现在情况下很明显我们需要在提交任务到线程池前,把父线程中的线程变量保存到任务内,然后等线程池内线程执行任务前把保存的父线程的线程变量复制到线程池中的执行线程上,然后运行我们的任务,等任务运行完毕后,在清除掉执行线程上的线程变量.可知第一我们需要包装提交到线程池内的任务,里面添加一个变量来保存父线程的线程变量.没错,TransmittableThreadLocal就是这样的思路,只是限于包权限限制,其内部做了一层缓存holder以便获取线程中保存的线程变量
  如上代码就是把我们的任务使用TtlRunnable.get(task);包装了下,其内部就是拷贝父线程中的线程变量到包装的任务内保存起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static TtlRunnable get(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
return get(runnable, releaseTtlValueReferenceAfterRun, false);
}
public static TtlRunnable get(Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
....
return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
}


private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
this.capturedRef = new AtomicReference<Object>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}


public static Object capture() {
Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
captured.put(threadLocal, threadLocal.copyValue());
}
return captured;
}

可知capture()方法就是把父线程内的线程变量获取过来,最后保存到了包装任务TtlRunnable内的capturedRef,另外TtlRunnable内runnable是我们实际要提交的任务.一个问题,holder中的threadLocal什么时候塞进入的,这个就是在代码4的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
public final void set(T value) {
super.set(value);
if (null == value) { // may set null to remove value
removeValue();
} else {
addValue();
}
}
private void addValue() {
if (!holder.get().containsKey(this)) {
holder.get().put(this, null); // WeakHashMap supports null value.
}
}

可知holder里面key主要保存了当前的TransmittableThreadLocal变量的引用.最后我们看TtlRunnable的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
public void run() {
Object captured = capturedRef.get();//A
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
Object backup = replay(captured);//B
try {
runnable.run();//C
} finally {
restore(backup);//D
}
}

代码A获取我们保存的父线程里面的线程变量,代码B设置到当前线程里面,代码C执行我们提交的任务,代码D恢复执行线程上下文

文章作者: Eric Liang
文章链接: https://ericql.github.io/2019/11/12/01-Java%E5%9F%BA%E7%A1%80%E7%AF%87/01-%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%82%B9/ThreadLocal/%E5%A2%9E%E5%BC%BA%E7%89%88%E7%9A%84ThreadLocal----TransmittableThreadLocal/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Eric Liang
打赏
  • 微信
  • 支付宝