ThreadLocal 提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:
1)存储单个线程上下文信息。比如存储id等;
2)使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;
3)减少参数传递。比如做一个 trace 工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便 debug。由于需要在工程各处随时取用,可放入 ThreadLocal。
原理
每个Thread内部都有一个 Map,我们每当定义一个 ThreadLocal 变量,就相当于往这个 Map 里放了一个 key,并定义一个对应的 value。每当使用 ThreadLocal,就相当于 get(key),寻找其对应的 value。话不多说,用图形表示大概是这样的:
下面我们简单分析 ThreadLocal 源码:
ThreadLocal 通过 threadLocalHashCode 来标识每一个 ThreadLocal 的唯一性。threadLocalHashCode 通过 CAS 操作进行更新,每次 hash 操作的增量为 0x61c88647。
接下来,看看它的 set 方法:
通过 Thread.currentThread() 方法获取了当前的线程引用,并传给了 getMap(Thread) 方法获取一个 ThreadLocalMap 的实例。我们继续跟进 getMap(Thread) 方法:
可以看到 getMap(Thread) 方法直接返回Thread实例的成员变量 threadLocals。它的定义在 Thread 内部,访问级别为 package 级别:
到了这里,我们可以看出,每个Thread里面都有一个 ThreadLocal.ThreadLocalMap 成员变量,也就是说每个线程通过 ThreadLocal.ThreadLocalMap 与 ThreadLocal 相绑定,而 ThreadLocal 是 ThreadLocalMap的key,这样可以确保每个线程访问到的 thread-local 变量都是本线程的。
我们往下继续分析。获取了 ThreadLocalMap 实例以后,如果它不为空则调用 ThreadLocalMap.ThreadLocalMap 的 set 方法设值;若为空则调用ThreadLocal 的 createMap 方法 new 一个 ThreadLocalMap 实例并赋给 Thread.threadLocals。
ThreadLocal 的 get 方法,源码如下:
通过 Thread.currentThread() 方法获取了当前的线程引用,并传给了 getMap(Thread) 方法获取一个 ThreadLocalMap 的实例。继续跟进 setInitialValue() 方法:
首先调用 initialValue() 方法来初始化,然后 通过 Thread.currentThread() 方法获取了当前的线程引用,并传给了getMap(Thread) 方法获取一个ThreadLocalMap 的实例,并将 初始化值存到 ThreadLocalMap 中。
initialValue() 源码如下:
下面我们探究一下ThreadLocalMap的实现。ThreadLocalMap是ThreadLocal的静态内部类,源码如下:
其中INITIAL_CAPACITY代表这个 Map 的初始容量;table 是一个 Entry 类型的数组,用于存储数据;size 代表表中的存储数目;threshold 代表需要扩容时对应 size 的阈值。
Entry 类是 ThreadLocalMap的 静态内部类,用于存储数据。Entry类继承了WeakReference,即每个Entry对象都有一个ThreadLocal的弱引用(作为key),这是为了防止内存泄露。一旦线程结束,key变为一个不可达的对象,这个Entry就可以被GC了。接下来我们来看ThreadLocalMap 的set方法的实现:
首先获取对应 ThreadLocal 在 table 当中的下标 key.threadLocalHashCode & (len-1),从该下标开始循环遍历,如遇相同 key,则直接替换 value;如果该 key 已经被回收失效,则替换该失效的 key;如果 entry 不存在,则从索引处开始找到第一个空的地方插入一个 Entry对象。最后判断是否需要进行扩容操作。
总结ThreadLocal 几点思考
1、为什么key使用弱引用?为什么还有内存泄漏问题,怎么规避?
不妨反过来想想,如果使用强引用,当 ThreadLocal 对象的引用被回收了,ThreadLocalMap( ThreadLocalMap 由 Thread 持有)本身依然还持有 ThreadLocal 的强引用,如果没有手动删除这个 key,则 ThreadLocal 不会被回收,所以只要当前线程不消亡,ThreadLocalMap 引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。
如果使用弱引用,那指向 ThreadLocal 对象的引用就两个:定义变量强引用和 ThreadLocalMap 中 Entry 的弱引用。一旦变量被回收,则指向ThreadLocal 的就只有弱引用了,在下次 gc 的时候,这个 ThreadLocal 就会被回收。
问题来了,ThreadLocal 对象只是作为 ThreadLocalMap 的一个key而存在的,现在它被回收了,但是它对应的 value 并没有被回收,内存泄露依然存在!而且key被删了之后,变成了 null,变成 (null,value) 的形式,value 更是无法被访问到了!针对这一问题,ThreadLocalMap 类的设计本身已经有了这一问题的解决方案,那就是在每次 set()/remove ()/get() ThreadLocalMap 中的值的时候,会自动清理 key 为 null 的 value。如此一来,value也能被回收了。当然建议的最佳实践是:在执行了 ThreadLocal.set() 方法之后一定要记得使用 ThreadLocal.remove(),将不要的数据移除掉,避免内存泄漏。
2、为什么不对value使用弱引用?
假设往 ThreadLocalMap 里存了一个 value,gc 过后 value 便消失了,那就无法使用 ThreadLocalMap 来达到存储全线程变量的效果了。
3、ThreadLocal 和线程池一起使用?
在线程复用的情况下,threadLocal 并不能保证按照预期执行,很有可能出现数据错乱。原因就是线程池中的线程在还未销毁的情况下,新的请求进来,会继续复用线程池中的线程,而这些线程在之前处理的过程中,对应的threadLocal有可能已经有值,导致出错,一般不建议将两者一起使用。
4、为什么网上大量使用 ThreadLocal 的源码都会加上 private static?
几个方面来回答吧:
1)static 修饰的变量在类的装载时候才会被加载,卸载时候才会被释放,所有该类的实例共享次变量。如果大量使用不带 static 的对象会造成内存的浪费。private 主要是从安全的角度考虑,防止对 ThreadLocal 的篡改。
2)ThreadLocal 实现方式实际上是通过 Thread 的 ThreadLocalMap 去保存的。引用的方式是弱引用,如果我们创建的大量的 ThreadLocal 对象, 当虚拟机回收这些对象的时候也就是 key 被回收了,但是 value 还在,变成 (null,value) 的形式,容易造成内存泄漏。
5、如何解决 Spring Cloud 中 Hystrix 线程隔离导致ThreadLocal数据丢失?
先解释原因:当隔离模式为线程时,Hystrix 会将请求放入 Hystrix 的线程池中去执行,这个时候某个请求就有 A 线程变成 B 线程了,ThreadLocal 必然消失了。
如何解决:使用 InheritableThreadLocal 代替 ThreadLocal,InheritableThreadLocal 原理也比较简单,就是在创建子线程时,将父线程的 inheritableThreadLocals 赋值给子线程。但对于使用线程池等会缓存线程的组件的情况,线程由线程池创建好,并且线程是缓存起来反复使用的;这时父子线程关系的 ThreadLocal 值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的 ThreadLocal 值传递到任务执行时。
在线程池的情况下,可以使用 TransmittableThreadLocal,是阿里开源的,用于解决 “在使用线程池等会缓存线程的组件情况下传递 ThreadLocal ”问题的 InheritableThreadLocal 扩展,原理也不复杂,在 Runnable 上再包裹了一层。至于在 Spring Cloud 中如何改造,大家可以自行搜索下,内容也必将多,就不做阐述了。
6、FastThreadLocal是Netty提供的,有兴趣可以了解下,mark下