ThreadLocal
1、ThreadLocal是什么
ThreadLocal就是线程本地变量,若创建了一个ThreadLocal变量,那访问这个变量的每个线程都会有这个变量的本地拷贝,但多个线程操作这个变量时,实际是操作自己本地内存里的变量,可以起到线程隔离的作用,避免了线程安全问题。
//创建一个ThreadLocal变量localVariable / /创建⼀个ThreadLocal变 量 public static ThreadLocal < String > localVariable = new ThreadLocal < > ();//写入:线程可以在任何地方使用localVariable localVariable.set("xxxx");//读取:线程在任何地方读取的都是它写入的变量 localVariable.get(); //xxxx
2、你在工作中用到过ThreadLocal吗
用到过,比如在登陆的时候,用户每次访问接口在请求头都会携带一个token,在控制层可以根据这个token,解析出用户的基本信息。由于在后面的服务层、持久层都会用到责怪用户信息,这时候就可以用到ThreadLocal,在控制层拦截请求把用户信息存入ThreadLocal,这样在其他任何地方都可以取出T和read Local中存的用户数据。
很多其他场景如cookie、session、数据库连接池都可以用ThreadLocal。
3、ThreadLocal怎么实现的
每个Thread对象里,有一个成员变量ThreadLocal.ThreadLocalMap threadLocals = null; 说明每个线程都有一个属于自己的ThreadLocalMap。当调用threadLocal.set(value)
时,会发生:
- 先获取当前线程 Thread t = Thread.currentThread();
- 再拿到该线程ThreadLocalMap
- 把数据存进去,形式是
<key, value>
那这里的key和value是什么?value就是set进去的对象。key不是ThreadLocal本身,而是ThreadLocal
的一个 弱引用。
那为什么是弱引用呢?假如key是强引用,若某个ThreadLocal
对象没有外部引用了(ThreadLocal = null),但ThreadLocalMap还持有它,那它就永远不会被GC,造成内存泄露。用了弱引用之后,一旦外部不再持有ThreadLocal,GC就会把它回收。ThreadLocalMap中的key会变成null,只剩下value。JVM之后会清理这些key为null的Entry,避免泄露。
4、ThreadLocal内存泄露是怎么回事
- key是弱引用:
- 外部不再引用
ThreadLocal
对象,GC 会回收它。ThreadLocalMap里的entry变成<null, value>,value还在,但程序员无法通过ThreadLocal拿到这份数据。若线程是线程池里的长生命周期线程,这块value会一直留在内存,直到线程结束才可能释放-->内存泄露
- 外部不再引用
- key是强引用:
- 即使外部不再引用 ThreadLocal,它也不会被 GC,因为 map 还持有强引用。
- 弱引用可以减轻泄露风险。
- 如何避免内存泄露(最佳实践)
try {local.set(new User("Alice"));// 业务逻辑 } finally {local.remove(); // ✅ 主动清理,避免泄漏 }
5、ThreadLocalMap的结构了解吗
ThreadLocalMap是一个定制化的Map,存放在Thread对象里,每个Thread维护一个自己的ThreadLocalMap,里面的key就是弱引用ThreadLocal。它没有实现Map接口(是内部类,只服务于ThreadLocal),主要是一个Entry[] table数组(每个 Entry 保存 <ThreadLocal弱引用, value>
)。
每次创建新的ThreadLocal对象,都会分配一个threadLocalHashCode值。这个值不是简单的1,2,3...自增,而是每次递增一个特殊的常数0x61c88647。这个数来自黄金分割数(√5 - 1) / 2 ≈ 0.618...。这样可以让哈希值分布更均匀,避免冲突集中。
6、ThreadLocalMap怎么结局hash冲突的
ThreadLocalMap使用开放定址法,这个坑被人占了就去接着找空着的坑。若插入一个value,通过hash计算后应该落入某个槽位,但这个坑已经被占了,且Entry数据的key和当前不相等,此时会线性向后查找,一直找到为null的槽位才会停止。
get的时候,也会根据ThreadLocal对象的hash值定位到table中的位置,然后判断该槽位Entry对象中的key是否和get的key一致,若不一致,就判断下一个位置。
7、ThreadLocal扩容机制了解吗
在ThreadLocalMap.set()
里,若存入元素时发现表里的Entry数量达到阈值(len*2/3),就会触发rehash()。
- 清理掉已经失效(key=null)的Entry
- 如果清理后size依然>=3/4 * threshold,就触发resize()扩容。
private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;int newLen = oldLen * 2; // 新数组长度翻倍Entry[] newTab = new Entry[newLen];for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null; // key 已被回收,帮助 GC} else {// 重新计算哈希位置int h = k.threadLocalHashCode & (newLen - 1);while (newTab[h] != null) { // 开放地址法,找下一个空位h = nextIndex(h, newLen);}newTab[h] = e; // 放到新数组}}}table = newTab; // 指向新数组 }
- 新数组翻倍:N->2N,降低负载因子
- 遍历老数组:把旧数组里的Entrty一个个搬到新数组。若key已经被GC,就清理掉value
- 重新计算位置:用新数组长度newLen重新取模
- 冲突处理:若目标格子被占,就调用nextIndex()往后找下一个空位(开放地址法)
- 更新引用:搬运完毕后,把table指向newTab。
8、父子线程怎么共享数据
- 普通ThreadLocal不能传递给子线程,因为ThreadLocal的值存放在当前对象的ThreadLocals变量里,就算是父线程,也不算是同一个线程。
- 解决办法:在Thread类里除了threadLocals之外,还有一个:ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 关键点在于子线程初始化时,从父线程的InheritableThreadLocalMap拷贝了一份数据。
public class InheritableThreadLocalTest {public static void main(String[] args) {ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();// 父线程设置值threadLocal.set("父线程的值");// 子线程new Thread(() -> {System.out.println("子线程获取:" + threadLocal.get());}).start();} }
//子线程获取:父线程的值
- 限制:只是在创建子线程那一刻复制,后续修改不同步。
- 线程池问题:线程池里的线程是复用的,子线程不会每次都重新init(),所以默认的InheritableThreadLocal在线程池场景可能会出问题。为解决这个,阿里开源了TransmittableThreadLocal (TTL),专门用于线程池下传递上下文。
参考
[1] 沉默王二公众号