ThreadLocal的一点探索

ThreadLocal的一点探索

捡破烂的诗人 781 2022-08-06
  • 联系方式:1761430646@qq.com
  • 编写时间:2022年8月6日15:42:21
  • 菜狗摸索,有误勿喷,烦请联系

1. ThreadLocal 引出

  1. 现在假设一个场景,在我们的程序内部,某个线程的执行链如下

  2. 假设如果在方法B执行时,我们得到了数据X,而这个数据X需要在方法E中用到

  3. 解决方法:

    1. 通过方法参数依次传递

      • 也即将数据X当做方法C的方法参数,这样方法C就可得到数据X
      • 再将数据X当做方法D的方法参数,这样方法D就可得到数据X
      • 依次类推
      • 最终方法E可获得数据X
      • 缺点:
        • 某个方法的方法参数应只包含本部分逻辑处理需求值,很明显,通过方法参数这种形式传递,对于方法C方法D来说,造成不必要的方法参数传递
        • 如果这条执行链很长,也就方法B方法E间隔很远,那么就会显得很杂乱,逻辑模糊,同时方法参数不好管理,比如说某两个方法之间又存在数据共享,那么这两个方法之间的所有方法又得加上这个共享数据作为方法参数–人麻了
    2. 通过一个专门用来存储这条执行链共享数据的工具类来解决

      ThreadLocal-工具类

      • 缺点:
        • 一个存储工具类对应一条执行链,但实际开发中很有可能是超级多执行链,如果构建多个存储工具类的话,在实现、管理方法就很繁琐–程序猿最喜欢的就是偷懒了
        • 还有个超级致命问题就是,对于同一条执行链,在并发场景下,会有多个线程对同一数据进行读写,会有线程安全问题
    3. 也就是我们本期的主人公:ThreadLocal

      • 这里其实也就借用了上述 2 的存储介质的思想

      • 在这里先要知道在Java中,Thread 代表的是线程,也就我们可以通过Thread来进行线程的操作

      • 对于上述

        • 一个线程对应有它自己的执行链
        • 在执行链内需要借助存储介质来达到数据共享
        • 同一条执行链会有多个线程执行,要防止对共享数据进行并发修改问题
      • 可知,这里很多问题都牵扯到了线程

      • 那么我们试想一下,是否可以使用线程Thread来作为存储介质?

      • 如果采用了线程Thread来作为存储介质

        • 首先,这里解决了共享数据的存储问题

        • 其次,对于某个线程来说,它有自己的执行链,对于另外的线程,不管其执行链不同还是相同,操作时我操作的都是当前线程存储的数据,不管其他线程屁事

        • 这样即保证了共享数据传输,又保证了共享数据的线程安全。(数据存放在其所在线程Thread中(可以理解为这个数据是线程私有的),不同线程之间互不影响)

2. ThreadLocal 是什么

  • 官方点说法:即线程本地量,它的核心思想是共享变量在每个线程都有一个副本多线程操作这个变量的时候,实际上都是在操作当前线程的副本,对另外的线程没有影响,从而起到了线程隔离的作用,避免了并发场景下的线程安全问题。
  • 个人理解其实就是一个工具类,解决了线程内的数据共享问题,省去了我们将共享数据存入/取出Thread时的一些复杂操作

3 ThreadLocalMap

  • ThreadLocal类中有一个静态内部类ThreadLocalMap,相当于变种的HashMap

  • 而在ThreadLocalMap内部的静态内部类Entry则是封装的键值对,其kThreadLocal类型,v为我们存储的数据类型

  • 很重要的一点是其k是弱引用,后面会有篇幅说这个问题

  • ThreadLocalMap内部是采用链表冲突法解决hash冲突的

  • 其结构如下图:

  • 作用:作为线程Thread中存储介质的类型

4. ThreadLoca 常用方法

4.1 get()

  • 	public T get() {
            // 获取当前线程
            Thread t = Thread.currentThread();
            // 获取当前线程的存储载体 ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                // map 已经初始化则根据当前 ThreadLocal 对象作为方法参数,也即是为 K,获取其封装的键值对
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    // 其键值对不为 null,直接返回对应的 V
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            // 当前线程的存储载体未实例化/不存在当前ThreadLocal对象作为 K 的键值对,走初始化逻辑
            return setInitialValue();
        }
    

4.2 initialValue()

  • 	protected T initialValue() {
            return null;
        }
    
  • 作用:设置每个键值对的v默认值都为null

4.3 setInitialValue()

  • 	private T setInitialValue() {
            // 设置键值对的`v`默认为 null
            T value = initialValue();
            // 获取当前线程
            Thread t = Thread.currentThread();
            // 获取当前线程的存储载体 ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null)
                // map 已经初始化,则直接设置插入这个键值对
                map.set(this, value);
            else
                // map 还未实例化,走创建逻辑
                createMap(t, value);
            // 返回默认的`v`,即为`null`
            return value;
        }
    
  • 作用:走初始化流程

    • 当前线程的存储载体还没初始化的话走创建逻辑,并把当前ThreadLocal对象作为knull作为v,存放到ThreadLocalMap中去;
    • 如果当前线程的存储载体已经初始化的,还是把把当前ThreadLocal对象作为knull作为v,存放到ThreadLocalMap中去

4.4 createMap(Thread t, T firstValue)

  • 	void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
  • 作用:为当前线程实例化其存储载体ThreadLocalMap,并把当前ThreadLocal对象作为k,null作为v,存入到ThreadLocalMap中去

4.5 nextIndex(int i, int len)

  • /**
     * i : 为当前索引位置
     * len:为数组的长度
     */
    private static int nextIndex(int i, int len) {
            // 返回在当前数组中索引 i 的下一个位置,如果达到数组末尾,则重头来
            return ((i + 1 < len) ? i + 1 : 0);
        }	
    

4.6 remove()

  • 	public void remove() {
            // 获取当前线程的存储载体`ThreadLocalMap`
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 // 在 map 中删除以当前`ThreadLocal`对象作为`k`的键值对
                 m.remove(this);
         }
    

4.7 remove(ThreadLocal<?> key)

  • 		private void remove(ThreadLocal<?> key) {
                // 获取其数组
                Entry[] tab = table;
                // 获取数组长度
                int len = tab.length;
                // 计算当前`ThreadLocal`作为`k`时,其键值对在数组中的位置
                int i = key.threadLocalHashCode & (len-1);
                // 遍历数组,找到对应键值对--由于采用链表冲突法,所以一开始计算得到的索引位置由于hash冲突的原由,可能不是当前`ThreadLocal`对象作为`k`对应的键值对的实际存储位置
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    if (e.get() == key) {
                        e.clear();
                        expungeStaleEntry(i);
                        return;
                    }
                }
            }
    

4.8 getMap(Thread t)

  • 	ThreadLocalMap getMap(Thread t) {
            // 获取当前线程的存储载体`ThreadLocalMap`
            return t.threadLocals;
        }
    

4.9 set(T value)

  • 	public void set(T value) {
            // 获取当前线程
            Thread t = Thread.currentThread();
            // 获取当前线程的存储载体`ThreadLocalMap`
            ThreadLocalMap map = getMap(t);
            if (map != null)
                // map 以实例化则直接存入数据
                map.set(this, value);
            else  
                // map 还为实例化则先进行实例化--实例化时会把当前数据第一次存入
                createMap(t, value);
        }
    

5. ThreadLocal 应用场景

  1. Spring事务中,保证一个线程下,一个事务的多个对数据的操作拿到的是同一个Connection
  2. 获取当前登录用户上下文

6. 内存泄漏问题–瞎搞,不严谨

  • 先来看看其结构图

    ThreadLocal-内存泄漏

  • 很明显的一点是设计者们设计成将Key弱引用指向ThreadLocal对象

  • 为什么设计成弱引用,不妨我们现在假设为强引用

    • ThreadLocal-内存泄漏-2

    • 在代码中,如果我们设置ThreadLocal变量为null,即想销毁这个对象时

      ThreadLocal-内存泄漏-3

    • JVM发生GC时,虽然说当前ThreadLocal变量已经设置为null,不再引用这ThreadLocal对象

    • 但是Key还是在强引用着ThreadLocal对象

    • 所以,GC时是根本不会回收这个ThreadLocal对象

    • 也就是一致存在着这样一条引用链:Thread变量–>Thread对象–>ThreadLocalMap–>Entry–>Key–>ThreadLocal对象

    • 导致ThreadLocal对象一直回收不了,也就是所谓的内存泄漏

  • 而当设计成弱引用时,按照上述逻辑走,ThreadLocal对象就能被正常回收

  • 这时候在ThreadLocalMap中就存在了其keynullEntry对象

  • 还存在着一条强引用链:Thread变量–>Thread对象–>ThreadLocalMap–>Entry–>Value

    ThreadLocal-内存泄漏-4

  • 试想一下,当我们在代码中手动设置ThreadLocal对象为null时,我们希望在GC时不仅此对象被回收,还希望其对应的Entry被回收(key都为null了,这时候这个Entry对象存在的意义的不大)

  • 但是现在看起来好像不行,并且Value一直被Entry对象强引用着,想回收Value都回收不了,还是有内存泄漏问题

  • 而设计者们早已想到这点,所以它们设计成当下一次调用ThreadLocalMapset()get()、方法时,就会手动去除这些keynullEntry对象

  • 这样Value少了被Entry对象强引用这一层,你想回收Value随时能回收,比如说直接设置为null

  • 现在看起来好像解决了这个内存泄漏问题

  • 但是,但是,有没有想过,在这个线程的后续声明周期中,我都没有再去调用对象的那几个方法

  • 那么这个没啥意思的Entry对象还存在着以及Value还是一致被强引用着,想回收都回收不了,还是有内存泄漏问题

  • 嗯哼?这时候咋搞

  • 在业务代码中手动调用ThreadLocalMap对象的remove()方法,自己去控制–这时候就考研程序员的功力了

  • remove()方法中会把Entry中的KeyValue都设置成null,这样就能被GC及时回收,无需触发额外的清理机制

7. 父子线程共享数据

  • 使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal

  • 使用:

  • 实现原理:

    • Thread 类除了含有存储当前线程共享数据载体的threadLocals

    • 还有存储父子线程共享数据载体的inheritableThreadLocals

    • 很重要的一点:在线程的init方法中,当判断父线程的inheritableThreadLocals不为空时,会把父线程的inheritableThreadLocals复制给子线程

8. 总结

  • 人麻了,不知所云
  • 现在所处水平的理解基本到这了,特别感觉内存泄漏逻辑有问题,以后再继续深入
  • 至少ThreadLocalMap的数组大小设计16,也就是2的n次幂,HashMap中有叙述到

9. 参考

  1. ThreadLocal的八个关键知识点–捡田螺的小男孩

  2. ThreadLocal夺命11连问–捡田螺的小男孩

  3. 鹅厂一面,有关ThreadLocal的一切


# 源码 # Java # ThreadLocal # 线程内数据共享