工作中对InheritableThreadLocal使用的思考

作者:京东保险 王奕龙

代码评审时,发现在线程池中使用InheritableThreadLocal上下文会使其中的线程变量失效,无法获取到预期的变量值,所以对问题进行了复盘和总结。

1. 先说结论

InheritableThreadLocal 只有在父线程创建子线程时,在子线程中才能获取到父线程中的线程变量;当配合线程池使用时: “第一次在线程池中开启线程,能在子线程中获取到父线程的线程变量,而当该子线程开启之后,发生线程复用,该子线程仍然保留的是之前开启它的父线程的线程变量,而无法获取当前父线程中新的线程变量” ,所以会发生获取线程变量错误的情况。

2. 实验例子

  • 创建一个线程数固定为1的线程池,先在main线程中存入变量1,并使用线程池开启新的线程打印输出线程变量,之后更改main线程的线程变量为变量2,再使用线程池中线程(发生线程复用)打印输出线程变量,对比两次输出的值是否不同
<!---->
1/** 2 * 测试线程池下InheritableThreadLocal线程变量失效的场景 3 */ 4public class TestInheritableThreadLocal { 5 6 private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>(); 7 8 // 固定大小的线程池,保证线程复用 9 private static final ExecutorService executorService = Executors.newFixedThreadPool(1); 10 11 public static void main(String[] args) { 12 threadLocal.set("main线程 变量1"); 13 // 正常取到 main线程 变量1 14 executorService.execute(() -> System.out.println(threadLocal.get())); 15 16 threadLocal.set("main线程 变量2"); 17 // 线程复用再取还是 main线程 变量1 18 executorService.execute(() -> System.out.println(threadLocal.get())); 19 } 20}

输出结果:

main线程 变量1
main线程 变量1

发现两次输出结果值相同,证明发生线程复用时,子线程获取父线程变量失效

3. 详解

3.1 JavaDoc

This class extends ThreadLocal to provide inheritance of values from parent thread to child thread: when a child thread is created, the child receives initial values for all inheritable thread-local variables for which the parent has values. Normally the child's values will be identical to the parent's; however, the child's value can be made an arbitrary function of the parent's by overriding the childValue method in this class.
Inheritable thread-local variables are used in preference to ordinary thread-local variables when the per-thread-attribute being maintained in the variable (e.g., User ID, Transaction ID) must be automatically transmitted to any child threads that are created.

InheritableThreadLocal 继承了 ThreadLocal, 以能够让子线程能够从父线程中继承线程变量: 当一个子线程被创建时,它会接收到父线程中所有可继承的变量。通常情况下,子线程和父线程中的线程变量是完全相同的,但是可以通过重写 childValue 方法来使父子线程中的值不同。

当线程中维护的变量如UserId, TransactionId 等必须自动传递到新创建的任何子线程时,使用InheritableThreadLocal要优于ThreadLocal

3.2 源码

1public class InheritableThreadLocal<T> extends ThreadLocal<T> { 2 /** 3 * 当子线程被创建时,通过该方法来初始化子线程中线程变量的值, 4 * 这个方法在父线程中被调用,并且在子线程开启之前。 5 * 6 * 通过重写这个方法可以改变从父线程中继承过来的值。 7 * 8 * @param parentValue the parent thread's value 9 * @return the child thread's initial value 10 */ 11 protected T childValue(T parentValue) { 12 return parentValue; 13 } 14 15 ThreadLocalMap getMap(Thread t) { 16 return t.inheritableThreadLocals; 17 } 18 19 void createMap(Thread t, T firstValue) { 20 t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); 21 } 22}

其中childValue方法来获取父线程中的线程变量的值,也可通过重写这个方法来将获取到的线程变量的值进行修改。

getMap方法和createMap方法中,可以发现inheritableThreadLocals变量,它是 ThreadLocalMap,在Thread类

在这里插入图片描述

3.2.1 childValue方法

  1. 开启新线程时,会调用Thread的构造方法
<!---->
1 public Thread(ThreadGroup group, String name) { 2 init(group, null, name, 0); 3 }

2. 沿着构造方法向下,找到init方法的最终实现,其中有如下逻辑:为当前线程创建线程变量以继承父线程中的线程变量

<!---->
1/** 2 * @param inheritThreadLocals 为ture,代表是为 包含可继承的线程变量 的线程进行初始化 3 */ 4private void init(ThreadGroup g, Runnable target, String name, 5 long stackSize, AccessControlContext acc, 6 boolean inheritThreadLocals) { 7 ... 8 9 if (inheritThreadLocals && parent.inheritableThreadLocals != null) 10 // 注意这里创建子线程的线程变量 11 this.inheritableThreadLocals = 12 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 13 14 ... 15 16}

3. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)创建子线程 InheritedMap 的具体实现

createInheritedMap 方法,最终会调用到 ThreadLocalMap私有构造方法,传入的参数parentMap即为父线程中保存的线程变量

1 private ThreadLocalMap(ThreadLocalMap parentMap) { 2 Entry[] parentTable = parentMap.table; 3 int len = parentTable.length; 4 setThreshold(len); 5 table = new Entry[len]; 6 7 for (int j = 0; j < len; j++) { 8 Entry e = parentTable[j]; 9 if (e != null) { 10 @SuppressWarnings("unchecked") 11 ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); 12 if (key != null) { 13 // 注意!!! 这里调用了childValue方法 14 Object value = key.childValue(e.value); 15 Entry c = new Entry(key, value); 16 int h = key.threadLocalHashCode & (len - 1); 17 while (table[h] != null) 18 h = nextIndex(h, len); 19 table[h] = c; 20 size++; 21 } 22 } 23 } 24 }

这个方法会对父线程中的线程变量做拷贝,其中调用了childValue方法来获取/初始化子线程中的值,并保存到子线程中

  • 由上可见,可继承的线程变量只是在线程被创建的时候进行了初始化工作,这也就能解释为什么在线程池中发生线程复用时不能获取到父线程线程变量的原因

4. 实验例子流程图

在这里插入图片描述

  1. main线程set main线程 变量1时,会调用到InheritableThreadLocalcreateMap方法,创建 inheritableThreadLocals 并保存线程变量
  2. 开启子线程1时,会拷贝父线程中的线程变量到子线程中,如图示
  3. main线程set main线程 变量2,会覆盖主线程中之前set的mian线程变量1
  4. 最后发生线程复用,子线程1无法获取到main线程新set的值,仍然打印 main线程 变量1

5. 解决方案: TransmittableThreadLocal

使用阿里巴巴 TransmittableThreadLocal 能解决线程变量线程封闭的问题,测试用例如下,在线程池提交任务时调用TtlRunnableget方法来完成线程变量传递

1public class TestInheritableThreadLocal { 2 3 private static final TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<>(); 4 5 // 固定大小的线程池,保证线程复用 6 private static final ExecutorService executorService = Executors.newFixedThreadPool(1); 7 8 public static void main(String[] args) { 9 threadLocal.set("main线程 变量1"); 10 // 正常取到 main线程 变量1 11 executorService.execute(() -> System.out.println(threadLocal.get())); 12 13 threadLocal.set("main线程 变量2"); 14 // 使用TransmittableThreadLocal解决问题 15 executorService.execute(TtlRunnable.get(() -> System.out.println(threadLocal.get()))); 16 17 executorService.shutdown(); 18 } 19}

输出结果:
main线程 变量1
main线程 变量2


That's all.

点赞
收藏

评论区

加载中...

相关推荐

ThreadLocal源码解析及实战应用

ThreadLocal是一个关于创建线程局部变量的类。通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。ThreadLocal在设计之初就是为解决并发问题而提供一种方案,每个线程维护一份自己的数据,达到线程隔离的效果。

java 面试知识点笔记(十)多线程与并发

问:线程安全问题的主要诱因?1.存在共享数据(也称临界资源)2.存在多条线程共同操作这些共享数据解决方法:同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作互斥锁的特征:1.互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程协调机制,这样在同一时间只有一

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

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

JNIEnv解析

1.关于JNIEnv和JavaVM JNIEnv是一个与线程相关的变量,不同线程的JNIEnv彼此独立。JavaVM是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此该进程的所有线程都可以使用这个JavaVM。当后台线程需要调用JNInative时,在native库中使用全局变量保存JavaVM

由 Mybatis 源码畅谈软件设计(一):序

作者:京东保险王奕龙从接触软件开发以来,一直对写出优雅的代码抱有执念,工作半年时,偶然接触到《代码整洁之道》,爱不释手,一口气读完,并在很长的时间内践行其中的观点,但是在这践行期间缺少思考和复盘,更多的是一味地信奉和遵守其中的原则,写了不少当时自认为不错而

由 Mybatis 源码畅谈软件设计(七):从根上理解 Mybatis 一级缓存

作者:京东保险王奕龙本篇我们来讲一级缓存,重点关注它的实现原理:何时生效、生效范围和何时失效,在未来设计缓存使用时,提供一些借鉴和参考。1.准备工作定义实体publicclassDepartmentpublicDepartment(Stringid)thi

工作中对InheritableThreadLocal使用的思考 - HelloWorld