ThreadLocal 并不是 Thread,而是 Thread 局部变量。
它不是为了解决多线程访问共享变量,而是为了数据隔离;它为每个线程创建一个单独的变量副本,保证各个线程里的变量独立于其他线程内的变量。
1. ThreadLocal 流程简单梳理
当调用 ThreadLocal 的set
方法写入数据时,数据最终都是存储在调用set
方法时所在的线程绑定的Thread实例;java.lang.Thread
类中有一个 threadLocals 字段,用于存储 ThreadLocal 写入的数据(第一次看 ThreadLocal 源码时,容易被绕进去的地方, ThreadLocal 本身并不存储数据);
1 | public class Thread implements Runnable { |
java.lang.Thread
类的 threadLocals 字段初始化是在 ThreadLocal 中完成的,当调用 ThreadLocal 的set
或get
方法写入数据时,如果当前线程的Thread实例的 threadLocals 字段为空,会进行初始化。
1 | // java.lang.ThreadLocal#createMap |
ThreadLocalMap 的构造方法要求传入一个Key-Value,而 ThreadLocal 在创建 ThreadLocalMap 时,传入的Key是this,说明 ThreadLocalMap 的key类型是 ThreadLocal 。
ThreadLocalMap 并非Java.util
包下的HashMap,而是 ThreadLocal 定义的一个静态内部类。与HashMap的实现不同, ThreadLocalMap 使用开放定址法解决Hash冲突,是一个简单的Map实现。
与其说 ThreadLocalMap 是一个Map,不如说是一个数组;组数元素的类型为ThreadLocalMap.Entry
,在插入元素时,会根据元素持有的弱引用对象计算出要插入的数组下标。
实际的项目中,一个请求通常是在一个线程中完成的,在其整个链路处理中,可能会存在多个 ThreadLocal ;
例如:Spring事务处理的 ThreadLocal 、Dubbo调用(RpcContext)的 ThreadLocal ,即一个请求即用到Dubbo发起RPC调用,又用到事务注解操作数据库;
那么至少就存在两个 ThreadLocal 往一个线程Thread实例的 threadLocals 字段写数据。
ThreadLocalMap 的key作用就是区分同一个线程Thread对象中不同 ThreadLocal 写入的数据,实现数据隔离。
2. ThreadLocal 为什么会内存泄漏?
ThreadLocalMap 使用 ThreadLocal 的弱引用作为key;
如果一个 ThreadLocal 没有外部强引用引用它,那么GC时,这个 ThreadLocal 势必会被回收,这样一来, ThreadLocalMap 中就会出现key为null的Entry;
key为null的Entry永远不会被访问,如果当前线程迟迟不结束(比如正好用在线程池),这些key为null的Entry的value就会一直存在;
在不使用某个 ThreadLocal 对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的 ThreadLocalMap 对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
3. ThreadLocal测试示例
1 |
|
输出结果:
1 | 14:02:23.988 [Thread-0] INFO com.xxx.ThreadLocalMainApp02 - #999 当前线程名称:[Thread-0],内部数据:[9527] |
观察输出结果,main线程 和 Thread-0 两个线程输出了不同的值,但它们操作的实例对象却是同一个。