• ThreadLocal及InheritableThreadLocal基本原理及注意项


    背景

            同事在使用ThreadLocal时,需要把主线程中的ThreadLocal值传输到子线程(线程池维护)中,故使用了InheritableThreadLocal作为传输。后发现,主线程执行ThreadLocal.remove()后,子线程中的ThreadLocal并不会被remove(),导致线程池中维护的ThreadLocal存储的值一直不变。于是深入进行了研究。

    ThreadLocal原理

            要讲清楚InheritableThreadLocal原理,首先要知道ThreadLocal是如何做到在线程中存储变量的,因为两者本质上是一个东西,只是InheritableThreadLocal多了一个子线程可继承的功能(部分方法实现不同)。

            直觉上理解可能认为ThreadLocal是一个Map,保存着Thread和存储变量的一个映射,就像这样Map<Thread, V>,但实际不是。

            从Thread类的源码中,我们可以看到这样一个threadLocals变量

    1. public
    2. class Thread implements Runnable {
    3. ...
    4. /* ThreadLocal values pertaining to this thread. This map is maintained
    5. * by the ThreadLocal class. */
    6. ThreadLocal.ThreadLocalMap threadLocals = null;
    7. ...
    8. }

           ThreadLocalMap中里面有一个Entry类,类似我们熟知的HashMap中,也有一个Entry类一样,ThreadLocalMap中的Entry存的则是<ThreadLocal<?>, value>,保存着ThreadLocal和value的映射。

    1. static class ThreadLocalMap {
    2. ...
    3. static class Entry extends WeakReference<ThreadLocal<?>> {
    4. Object value;
    5. Entry(ThreadLocal<?> k, Object v) {
    6. super(k);
    7. value = v;
    8. }
    9. }
    10. ...
    11. private Entry[] table;
    12. ...
    13. }

            同时ThreadLocalMap中,有一个Entry数组table作为存储。那么我们可以画出实际上Thread和ThreadLocal的关系实际上是这样的。

            

             也就是一个Thread可以拥有多个ThreadLocal,通过ThreadLocalMap来进行存储,这就是ThreadLocal和Thread之间的关系。

            顺带补充一下,我们知道HashMap底层结构是数组+链表,用Key值的HashCode来计算数组中存放的index,用链表来解决哈希冲突,即链地址法。

            而ThreadLocalMap类似,利用ThreadLocal的hashcode来计算数组中存放的index,但是解决哈希冲突方法是,如果计算出来的index已存放了元素,那么index+1,越界时则重新回到0。当然这样最终会导致无地址可以用,再一直这样循环查找会导致死循环,所以当数组中元素超出一定阈值(threshold)时,就会进行扩容,此处便不再进行展开。

            前面讲了ThreadLocal和Thread的联系,那么调用ThreadLocal.set的时候,是如何做到保存到对应线程中的,调用get时候又是怎么取出来的。

    Set方法

            首先先看到 ThreadLocal的set源码:

    1. public void set(T value) {
    2. Thread t = Thread.currentThread();
    3. ThreadLocalMap map = getMap(t);
    4. if (map != null)
    5. map.set(this, value);
    6. else
    7. createMap(t, value);
    8. }
    9. ThreadLocalMap getMap(Thread t) {
    10. return t.threadLocals;
    11. }
    12. void createMap(Thread t, T firstValue) {
    13. t.threadLocals = new ThreadLocalMap(this, firstValue);
    14. }

             当调用set时,首先会获取到当前执行线程,并获取当前线程的 threadLocals 属性,即上文我们着重讲的ThreadLocalMap,若不存在则创建,然后以当前的ThreadLocal对象作为key,存入ThreadLocalMap中。大致流程图如下:

            

             这里我们也可以知道Thread中的ThreadLocalMap并不是构建出来就创建了的,而是需要用到的时候再去创建,这种懒加载的思想其实可以用到我们日常开发中,对于在某些判断分支下不需要创建的数组或对象不要提前进行创建。

    Get方法

            接下来看到 ThreadLocal的get源码:

    1. public T get() {
    2. Thread t = Thread.currentThread();
    3. ThreadLocalMap map = getMap(t);
    4. if (map != null) {
    5. ThreadLocalMap.Entry e = map.getEntry(this);
    6. if (e != null) {
    7. @SuppressWarnings("unchecked")
    8. T result = (T)e.value;
    9. return result;
    10. }
    11. }
    12. return setInitialValue();
    13. }
    14. private T setInitialValue() {
    15. T value = initialValue();
    16. Thread t = Thread.currentThread();
    17. ThreadLocalMap map = getMap(t);
    18. if (map != null)
    19. map.set(this, value);
    20. else
    21. createMap(t, value);
    22. return value;
    23. }
    24. ThreadLocalMap getMap(Thread t) {
    25. return t.threadLocals;
    26. }
    27. void createMap(Thread t, T firstValue) {
    28. t.threadLocals = new ThreadLocalMap(this, firstValue);
    29. }

            当调用get时,首先会获取当前执行线程,从线程中获取ThreadLocalMap,再从ThreadLocalMap中获取当前ThreadLocal对应的值。当获取不到ThreadLocalMap,或者当前ThreadLocal没有值时,则进行初始化,若当前线程ThreadLocalMap不存在,则创建当前线程的ThreadLocalMap,并存放入初始值(默认为null),若已存在,则存入当前ThreadLocal和初始值至ThreadLocalMap中。大致流程图如下:

            

             图上和代码上的判定分支流程略有差异,但意思一致,initialValue() 默认return null,所以这里用null表达,可以对照源码理解。

            至此,对ThreadLocal的基本原理我们有了大致上的理解,还有常用remove方法及其他建议自行阅读源码。这里仅对自己的研究做一个总结和分享。

            那么接下来,就可以研究 InheritableThreadLocal 是如何实现可继承性的。

    InheritableThreadLocal原理

            InheritableThreadLocal作用是:将在父线程中放入ThreadLocal的值,在子线程中继承使用。正常我们使用ThreadLocal是无法做到这一步的,我们可以写一段程序实验一下。

     

             这里我们看到输出了null,当然,这是必然的。ThreadLocal设计就是为了保存线程各自的本地变量互不干扰,如果两个线程能访问到同一个,那就变得线程不安全,也违背了它的设计。

            然后我们将ThreadLocal换成InheritableThreadLocal,再试试。

             可以看到获取ThreadLocal中的值成功了。那这样是否会导致线程不安全呢,还能叫线程本地变量么,答案是可以的。接下来看下InheritableThreadLocal的实现,我们就可以理解了。

            首先看Thread类中,可以看到有两个ThreadLocalMap,threadLocals 和inheritableThreadLocals。

            

    1. public
    2. class Thread implements Runnable {
    3. ...
    4. /* ThreadLocal values pertaining to this thread. This map is maintained
    5. * by the ThreadLocal class. */
    6. ThreadLocal.ThreadLocalMap threadLocals = null;
    7. /*
    8. * InheritableThreadLocal values pertaining to this thread. This map is
    9. * maintained by the InheritableThreadLocal class.
    10. */
    11. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    12. ...
    13. }

            从注释可以看到,简单来说,就是 ThreadLocal 的值放到 threadLocals 里,InheritableThreadLocal 的值放到 inheritableThreadLocals 里。

            那么我们首先可以有一个概念——InheritableThreadLocal 的值会放到 Thread的 inheritableThreadLocals 属性中进行操作。

    InheritableThreadLocal源码

            再来看 InheritableThreadLocal 的源码,可以看到它继承了 ThreadLocal,并重写了其中几个方法。它的可继承能力,就在它重写的几个方法中。

    1. public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    2. protected T childValue(T parentValue) {
    3. return parentValue;
    4. }
    5. ThreadLocalMap getMap(Thread t) {
    6. return t.inheritableThreadLocals;
    7. }
    8. void createMap(Thread t, T firstValue) {
    9. t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    10. }
    11. }

    大致看下这几个方法:       

    • childValue(): 直接返回传入的parentValue,这个看起来有点不明所以,之后再来展开 
    • getMap(Thread t): 返回了t.inheritableThreadLocals,结合之前的get,set源码的阅读,也就算说如果你是一个InheritableThreadLocal对象,那么它会从线程的inheritableThreadLocals来执行get,set操作
    • createMap(Thread t, T firstValue): 创建一个ThreadLocalMap,并赋值给线程的inheritableThreadLocals

            从上述getMap、createMap方法,结合我们之前看到get,set源码,那就可以理解到“InheritableThreadLocal 的值会放到 Thread的 inheritableThreadLocals 属性中进行操作”这句话的意义。

    childValue与parentMap浅拷贝

            那么childValue()是干嘛的呢?看下哪里会调到它,就会发现一片新的天地。顺着childValue点开调用它的地方,可以看到是一个私有构造函数。

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

            大致浏览一下,可以看到,它作用是把传入的 parentMap 浅拷贝到当前子线程的ThreadLocalMap中。

            那么再看下这个私有构造函数被谁调用,依次跟下来可以看到这样一个调用链。

            > new Thread(...)

                   > init(...)

                           >ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)

                                  >ThreadLocalMap(parentMap)

           

    init方法

            init方法会在构建Thread的时候执行,在 init 方法中,我们可以看到这么一段:

    1. private void init(ThreadGroup g, Runnable target, String name,
    2. long stackSize, AccessControlContext acc,
    3. boolean inheritThreadLocals) {
    4. ...
    5. Thread parent = currentThread();
    6. ...
    7. if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    8. this.inheritableThreadLocals =
    9. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    10. ...
    11. }

            当if条件不为 false 时,那么就会触发浅拷贝,将父线程中inheritableThreadLocals中的值复制到子线程的inheritableThreadLocals中。

            这里父线程 parent = currentThread(); 如果不理解的话,想一下,我们是从当前线程再创建线程,那么当前线程其实就是父线程了。

            这里我们再来看两个条件 inheritThreadLocals 和 parent.inheritableThreadLocals != null

            首先看 inheritThreadLocals,可以看到它是由方法入参来的,那么就看下它被调的时候,传了啥。     

            这里看到有两个地方调了它,有acc(AccessControlContext)传false,没有的时候传true,这里 AccessControlContext 是什么可以自行研究,正常new Thread()的时候,默认传true。

            搞定完 inheritThreadLocals,再来看 parent.inheritableThreadLocals != null,这个条件比较好理解。InheritableThreadLocal类重写了 createMap 和 getMap 方法,将所有操作指向了 Thread.inheritableThreadLocals。

            那么只要我们在父线程的 InheritableThreadLocal,调用get或set了,那么 parent.inheritableThreadLocals != null 就成立。

            上述两个条件 inheritThreadLocals && parent.inheritableThreadLocals != null 成立后,那就会触发浅拷贝到子线程的 inheritableThreadLocals 中了,在构建完成后,子线程便可以读取到父线程中ThreadLocal中的值。

            (注:父子线程中的 inheritableThreadLocals 并不共享,依旧保持ThreadLocal特性,仅仅是将父线程 inheritableThreadLocals 的元素浅拷贝到子线程中,子线程有自己的inheritableThreadLocals)

    注意项

    1. ThreadLocal与当前线程绑定,虽然底层实现是WeakReference,但如果不用的时候需及时进行remove,可加快垃圾回收,避免占用内存。
    2. InheritableThreadLocal 中是将父线程中的 inheritableThreadLocals 浅拷贝到到子线程中,代码上看就算不使用也会拷贝。(如理解有误请指正)
    3. 父子线程的 inheritableThreadLocals 并不共享,如果你在父线程中执行了remove,子线程中不会受影响,依旧可以get出来。如果有使用到判空get值做判空处理,需注意。

            

     

            

  • 相关阅读:
    使用 PyQT 和 Qt 设计器进行 Python GUI 开发
    基于springboot+vue的在线购房(房屋租赁)系统
    darknet c++源码阅读笔记-01-activation_kernels.cu
    java基础16 GUI编程(Swing和AWT)
    【前端】响应式布局笔记——rem
    咖啡│咖啡竟可助眠?一旦飲錯時間隨時影響整天血糖代謝
    创意电子学小知识:串联和并联
    快速求N!
    char指针和unsigned char指针间的转换
    并查集及实现
  • 原文地址:https://blog.csdn.net/bigwhitetao9527/article/details/125444894