Java 并发编程之美(五):揭开 InheritableThreadLocal 的面纱

前言

博客《Java 并发编程之美(四):深入剖析 ThreadLocal》提到 ThreadLocal 变量的基本使用方式,ThreadLocal 是一个本地线程副本变量工具类,主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。但是在实际的开发中,有这样的一种需求:父线程生成的变量需要传递到子线程中进行使用,那么在使用 ThreadLocal 似乎就解决不了这个问题。由于 ThreadLocal 设计之初就是为了绑定当前线程,如果希望当前线程的 ThreadLocal 能够被子线程使用,实现方式就会相当困难。在此背景下,InheritableThreadLocal 应运而生,使用 InheritableThreadLocal 这个变量就可以轻松的在子线程中依旧使用父线程中的本地变量。

ThreadLocal 与 InheritableThreadLocal 区别

ThreadLocal 声明的变量是线程私有的成员变量,每个线程都有该变量的副本,线程对变量的修改对其他线程不可见。

InheritableThreadLocal 声明的变量同样是线程私有的,但是子线程可以使用同样的 InheritableThreadLocal 类型变量从父线程继承 InheritableThreadLocal 声明的变量,父线程无法拿到其子线程的。即使可以继承,但是子线程对变量的修改对父线程也是不可见的。

对 InheritableThreadLocal 的理解

InheritableThreadLocal 类是 ThreadLocal 类的子类。ThreadLocal 中每个线程拥有它自己的值,与 ThreadLocal 不同的是,InheritableThreadLocal 允许一个线程以及该线程创建的所有子线程都可以访问它保存的值。

ThreadLocal 是不支持继承性的,所谓继承性也是针对父线程和子线程来说,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Scratch {public static final ThreadLocal<String> localVariable = new ThreadLocal<>();

public static void main(String[] args) {
localVariable.set("I'm variable in main");
System.out.println(Thread.currentThread().getName() + ":" + localVariable.get());

new Thread(() -> System.out.println(Thread.currentThread().getName() + ":" + localVariable.get())).start();
}
}

// Output
// main:I'm variable in main
// Thread-0:null

InheritableThreadLocal 用于子线程能够拿到父线程往 ThreadLocal 里设置的值,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Scratch {

public static final ThreadLocal<String> localVariable = new InheritableThreadLocal<>();

public static void main(String[] args) {
localVariable.set("I'm variable in main");
System.out.println(Thread.currentThread().getName() + ":" + localVariable.get());

new Thread(() -> System.out.println(Thread.currentThread().getName() + ":" + localVariable.get())).start();
}
}

// Output
// main:I'm variable in main
// Thread-0:I'm variable in main

深入解析 InheritableThreadLocal 类

InheritableThreadLocal 类重写了 ThreadLocal 的 3 个函数:

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
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* 该函数在父线程创建子线程,向子线程赋值 InheritableThreadLocal 变量时使用
* 可重写 childValue() 方法实现子线程与父线程之间互不影响
*/
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);
}
}

InheritableThreadLocal 赋值

从源码上看,跟 ThreadLocal 不一样的无非是 ThreadLocalMap 的引用不一样了,从逻辑上来讲,这并不能做到子线程得到父线程里的值。那么秘密在那里呢?通过跟踪 Thread 的构造方法,你能够发现是在构造 Thread 对象的时候对父线程的 InheritableThreadLocal 进行了赋值。下面是 Thread 的部分源码:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class Thread implements Runnable {

/**
* 默认人构造方法, 会调用 init 方法进行初使化
*/
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

/**
* 最终初始化线程的方法
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}

this.name = name;

// parent 为当前线程, 也就是调用了 new Thread(); 方法的线程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */

/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}

/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}

/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();

/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}

g.addUnstarted();

// 在这里会继承父线程是否为后台线程的属性还有父线程的优先级
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 这里是重点, 当父线程的 inheritableThreadLocals 不为空的时候, 会调用 ThreadLocal.createInheritedMap 方法, 传入的是父线程的 inheritableThreadLocals。原来复制变量的秘密在这里
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;

/* Set thread ID */
tid = nextThreadID();
}
}

通过跟踪 Thread 的构造方法,我们发现只要父线程在构造子线程(调用 new Thread())的时候 inheritableThreadLocals 变量不为空。新生成的子线程会通过 ThreadLocal.createInheritedMap 方法将父线程 inheritableThreadLocals 变量有的对象复制到子线程的 inheritableThreadLocals 变量上。这样就完成了线程间变量的继承与传递。

ThreadLocal.createInheritedMap

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
41
42
public class ThreadLocal<T> {

/**
* 根据传入的 map, 构造一个新的 ThreadLocalMap
*/
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}

static class ThreadLocalMap {
// 这个 private 的构造方法就是专门给 ThreadLocal 使用的
private ThreadLocalMap(ThreadLocalMap parentMap) {
// ThreadLocalMap 还是用 Entry 数组来存储对象的
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
// 创建跟父线程相同大小的 table
table = new Entry[len];
// 这里是复制 parentMap 数据的逻辑
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
// 得到父线程中变量对应的 key, 即 ThreadLocal 对象
ThreadLocal key = e.get();
if (key != null) {
// 此处会调用 InheritableThreadLocal 重写的方法, 默认直接返回入参值
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);
// 将 Entry 放在对应的位置
table[h] = c;
size++;
}
}
}
}
}
}

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Scratch {

private static final ThreadLocal<String> localVariable = new InheritableThreadLocal<>();

public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
CountDownLatch doneSignal = new CountDownLatch(2);

// 1.main 线程第一次赋值 "I'm variable_1 in main"localVariable.set("I'm variable_1 in main");
System.out.println(Thread.currentThread().getName() + ":" + localVariable.get());

// 2. 线程池执行方法, 查看线程中线程 InheritableThreadLocal 赋值情况
executorService.submit(new Worker(doneSignal));
executorService.submit(new Worker(doneSignal));

// 3.wait for all to finish[等待线程 pool-1-thread-1/pool-1-thread-2 执行完后, 在对主线程的 InheritableThreadLocal 进行赋值, 查看赋值后, 线程池中线程的 InheritableThreadLocal 是否发生变法]
doneSignal.await();

// 4.main 线程第二次赋值 "I'm variable_2 in main"localVariable.set("I'm variable_2 in main");
System.out.println(Thread.currentThread().getName() + ":" + localVariable.get());

// 5. 线程池执行方法, 查看线程中线程 InheritableThreadLocal 赋值情况
executorService.submit(new Worker(doneSignal));
executorService.submit(new Worker(doneSignal));

// 6. 关闭线程池
executorService.shutdown();
}

static class Worker implements Runnable {

private final CountDownLatch doneSignal;

Worker(CountDownLatch doneSignal) {
this.doneSignal = doneSignal;
}

@Override
public void run() {
doneSignal.countDown();
System.out.println(Thread.currentThread().getName() + ":" + localVariable.get());
}
}
}


// Output
// main:I'm variable_1 in main
// pool-1-thread-2:I'm variable_1 in main
// pool-1-thread-1:I'm variable_1 in main
// main:I'm variable_2 in main
// pool-1-thread-2:I'm variable_1 in main
// pool-1-thread-1:I'm variable_1 in main

前后两次调用获取的值是一开始赋值的值,因为线程池中是缓存使用过的线程,当线程被重复调用的时候并没有再重新初始化 init() 线程,而是直接使用已经创建过的线程,所以这里的值并不会被再次操作。因为实际的项目中线程池的使用频率非常高,每一次从线程池中取出线程不能够直接使用之前缓存的变量,所以要解决这一个问题,网上大部分是推荐使用 alibaba 的开源项目 transmittable-thread-local

transmittable-thread-local

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

在 ThreadLocal 的需求场景即是 TTL(装饰器模式)的潜在需求场景,如果你的业务需要『在使用线程池等会池化复用线程的组件情况下传递 ThreadLocal』则是 TTL 目标场景。下面是几个典型场景例子:1、分布式跟踪系统;2、日志收集记录系统上下文;3 应用容器或上层框架跨应用代码给下层 SDK 传递信息。

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.10.2</version>
</dependency>

代码示例:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Scratch {

private static final ThreadLocal<String> localVariable = new TransmittableThreadLocal<>();

public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 额外的处理,生成修饰了的对象 executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);

CountDownLatch doneSignal = new CountDownLatch(2);

// 1.main 线程第一次赋值 "I'm variable_1 in main"localVariable.set("I'm variable_1 in main");
System.out.println(Thread.currentThread().getName() + ":" + localVariable.get());

// 2. 线程池执行方法, 查看线程中线程 InheritableThreadLocal 赋值情况
executorService.submit(new Worker(doneSignal));
executorService.submit(new Worker(doneSignal));

// 3.wait for all to finish[等待线程 pool-1-thread-1/pool-1-thread-2 执行完后, 在对主线程的 InheritableThreadLocal 进行赋值, 查看赋值后, 线程池中线程的 InheritableThreadLocal 是否发生变法]
doneSignal.await();

// 4.main 线程第二次赋值 "I'm variable_2 in main"localVariable.set("I'm variable_2 in main");
System.out.println(Thread.currentThread().getName() + ":" + localVariable.get());

// 5. 线程池执行方法, 查看线程中线程 InheritableThreadLocal 赋值情况
executorService.submit(new Worker(doneSignal));
executorService.submit(new Worker(doneSignal));

// 6. 关闭线程池
executorService.shutdown();
}

static class Worker implements Runnable {

private final CountDownLatch doneSignal;

Worker(CountDownLatch doneSignal) {
this.doneSignal = doneSignal;
}

@Override
public void run() {
doneSignal.countDown();
System.out.println(Thread.currentThread().getName() + ":" + localVariable.get());
}
}
}

// Output
// main:I'm variable_1 in main
// pool-1-thread-2:I'm variable_1 in main
// pool-1-thread-1:I'm variable_1 in main
// main:I'm variable_2 in main
// pool-1-thread-1:I'm variable_2 in main
// pool-1-thread-2:I'm variable_2 in main

整个过程的完整时序图:

r45yw78ty

总结

  • ThreadLocal 和 InheritableThreadLocal 本质上只是为了方便编码给的工具类,具体存数据是 ThreadLocalMap 对象。
  • ThreadLocalMap 存的 key 对象是 ThreadLocal,value 就是真正需要存的业务对象。
  • Thread 里通过两个变量持用 ThreadLocalMap 对象,分别为:threadLocals 和 inheritableThreadLocals。
  • InheritableThreadLocal 之所以能够完成线程间变量的传递,是在 newThread() 的时候对 inheritableThreadLocals 对象里的值进行了复制。
  • 子线程通过继承得到的 InheritableThreadLocal 里的值与父线程里的 InheritableThreadLocal 的值具有相同的引用,如果父子线程想实现不影响各自的对象,可以重写 InheritableThreadLocal 的 childValue 方法。

参考博文

[1]. ThreadLocal 和 InheritableThreadLocal 深入分析
[2]. transmittable-thread-local
[3]. InheritableThreadLocal 详解


Java 并发编程之美系列


谢谢你长得那么好看,还打赏我!😘
0%