ThreadLocal 我看了好几遍才看懂,原来关键在引用上
学并发编程绕不过 ThreadLocal我第一次看的时候觉得哦就是给每个线程一份独立的数据嘛然后就跳过去了。结果后面面试被问到ThreadLocal 为什么会内存泄漏直接卡住。后来我发现不把底层原理理清楚光记住用完了要 remove()这句话下次遇到类似的问题还是会懵。于是又硬着头皮读了一遍源码这次总算理清楚了。它到底是干嘛的先说作用不然不知道学它干什么。一个东西给多个线程共用数据会乱。这是并发编程最原始的问题。解决思路大致有两种一是用锁让线程排队访问synchronized、Lock 这些二是给每个线程一份自己的数据大家各玩各的不用抢。ThreadLocal 就是后者。我遇到的场景是这样的一个 Web 请求进来经过拦截器、Controller、Service 好几层中间可能需要传递用户信息、traceId 这类上下文数据。如果每个方法都通过参数传代码会变得很啰嗦改起来也很痛苦。ThreadLocal 可以让你在同一个线程的任意地方取到之前存进去的数据不用层层传参。另外还有一个好处——它不用锁。每个线程拿自己的那份副本天然没有竞争所以高并发下性能比加锁好很多。底层结构——它到底把数据存哪了这一点我最初的理解是错的。我以为值存 ThreadLocal 对象本身里面后来才发现 ThreadLocal 只是一个工具人真正的数据存在当前线程对象Thread内部的一个成员变量里。具体是这样的Thread 对象 └── threadLocals类型是 ThreadLocalMap └── Entry 数组 ├── Entry 1: keyThreadLocal实例, value存的数据 ├── Entry 2: keyThreadLocal实例, value存的数据 └── ...也就是说每个 Thread 对象内部有一个叫threadLocals的 Map这个 Map 的 key 是 ThreadLocal 实例本身value 是你存进去的数据。所以同一个线程里可以有很多个 ThreadLocal 变量每个 ThreadLocal 在 Map 里是一条记录。Entry 的设计——我在这里卡了很久ThreadLocalMap 里面的 Entry 继承了WeakReferenceThreadLocal?。这句话我第一次看的时候没多想后来发现这才是整套设计最核心、也是最有争议的地方。Entry 的 key 是弱引用WeakReference而 value 是强引用。弱引用的意思是如果这个对象只被弱引用指向没有被其他地方强引用那么下次 GC 就可以回收它。为什么要用弱引用我当时想了很久。后来理解的是这样假设我们有一个ThreadLocalUserInfo userLocal new ThreadLocal()用完数据之后我们把userLocal null置空了。如果没有弱引用Entry 里的 key 还强引用着这个 ThreadLocal 对象那 GC 就永远回收不了它。有了弱引用userLocal null之后下一次 GC 就能把 ThreadLocal 对象回收掉key 变成 null。但问题在于key 被回收了value 还在啊。value 是强引用只要 Entry 还挂在 ThreadLocalMap 里而 ThreadLocalMap 又被 Thread 强引用着那这个 value 就永远释放不掉。尤其是在线程池的场景下——线程是长期存活、反复利用的Thread 对象一直活着这条 Entry 和它的 value 就一直不会释放。这就是 ThreadLocal 内存泄漏的根源。怎么解决官方给的解决办法很直接用完了显式调用remove()。try{userLocal.set(userInfo);// 执行业务逻辑}finally{userLocal.remove();}remove()方法会找到当前线程的 ThreadLocalMap以当前 ThreadLocal 为 key把对应的 Entry 整条删掉。这样 Entry 被移除了value 也就没有引用指向它了可以被正常回收。另外ThreadLocalMap 的get()和set()方法内部也做了一些顺手清理的工作——它们遍历的时候如果发现 key 为 null 的脏 Entry会顺手清理掉。但这只是辅助不能完全指望它。还有一个有意思的点——哈希冲突的处理方式ThreadLocalMap 解决哈希冲突的方式跟 HashMap 不一样。HashMap 用的是链表红黑树而 ThreadLocalMap 用的是线性探测法。线性探测什么意思就是算出来应该放在索引 5 的位置结果 5 已经被占了那就看 6、7、8……直到找到一个空位。取的时候也类似算出来位置如果 key 不是目标对象就往后一个个找。这种方式的缺点是如果 Map 里的数据多了探测链就会变长查找效率下降。所以单个线程里不要搞太多 ThreadLocal 变量。不过 ThreadLocal 在哈希值的生成上做了一些优化——它用了一个叫0x61c88647的魔法数来累加哈希值这个数跟斐波那契数列有关能让哈希值在 2 的幂次方大小的数组中分布得非常均匀尽量减少冲突。总结一下我学到的ThreadLocal 的核心价值是给每个线程一份独立的数据副本不用锁也能线程安全数据存在 Thread 内部的 ThreadLocalMap 里ThreadLocal 只是 keyEntry 是弱 key 强 value的设计这是内存泄漏的根源解决办法很简单finally 块里调 remove()这是铁律没得商量底层用线性探测法解决哈希冲突内部通过魔法数做了优化学这个东西的过程中我最大的感受是很多框架和工具类的最佳实践背后都有它存在的理由。如果只是记住用完了要 remove下次遇到为什么线程池用了 ThreadLocal 会内存泄漏还是不会。只有把引用链走一遍才知道那条链路到哪一步断了、到哪一步没断。上面哪里写得不准确欢迎指出——毕竟我也是慢慢看代码看明白的。