深入分析 ThreadLocal 内存泄漏问题
前言
ThreadLocal
的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。但是如果滥用 ThreadLocal
,就可能会导致内存泄漏。下面,我们将围绕三个方面来分析 ThreadLocal
内存泄漏的问题
ThreadLocal
实现原理ThreadLocal
为什么会内存泄漏ThreadLocal
最佳实践
ThreadLocal 实现原理
ThreadLocal
的实现是这样的:每个Thread
维护一个 ThreadLocalMap
映射表,这个映射表的 key
是 ThreadLocal
实例本身,value
是真正需要存储的Object
。
也就是说 ThreadLocal
本身并不存储值,它只是作为一个 key
来让线程从 ThreadLocalMap
获取 value
。值得注意的是图中的虚线,表示ThreadLocalMap
是使用 ThreadLocal
的弱引用作为 Key
的,弱引用的对象在 GC 时会被回收。
ThreadLocal
为什么会内存泄漏
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal
势必会被回收,这样一来,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
其实,ThreadLocalMap
的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里所有key
为null
的value
。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用线程池的时候,这个线程执行任务结束,
ThreadLocal
对象被回收了,线程放回线程池中不销毁,这个线程一直不被使用,导致内存泄漏。 - 分配使用了
ThreadLocal
又不再调用get()
,set()
,remove()
方法,那么这个期间就会发生内存泄漏。
为什么使用弱引用
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析为什么会内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用?为什么不用强引用?
我们先来看看官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
下面我们分两种情况讨论:
- key 使用强引用:引用的
ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry
内存泄漏。 - key 使用弱引用:引用的
ThreadLocal
的对象被回收了,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。value
在下一次ThreadLocalMap
调用set
,get
的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果都没有手动删除对应key
,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal
不会内存泄漏,对应的value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除。
因此,ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key
就会导致内存泄漏,而不是因为弱引用。
ThreadLocal 最佳实践
综合上面的分析,我们可以理解ThreadLocal
内存泄漏的前因后果,那么怎么避免内存泄漏呢?
- 每次使用完
ThreadLocal
,都调用它的remove()
方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal
,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal
就跟加锁完要解锁一样,用完就清理。
为什么会导致业务逻辑出错:
当ThreadLocal用于线程池、web应用或者线程被多次重复使用的时候,特别要注意,以web应用为例:
我们都知道web应用很多类都是单例模式,如spring默认注入方式所创建的类就是一个单例,当不同的http请求到达服务器的时候,实际上都是使用了同一个实例,假如该实例使用了全局变量,当请求A修改了这个变量,后面到来的其它请求(此时不管A请求是否结束),如请求B再使用该变量的时候,实际上是被请求A修改过的,这会导致业务逻辑出错,而且很难被发现,这种情况,通常是使用ThreadLocal来解决,因为不同的请求虽然使用了同一个实例,但所使用的线程却不同,但有一点需要特别注意,那就是web容器的线程是重复使用的,web容器使用了线程池,当一个请求使用完某个线程,该线程会放回线程池被其它请求使用,这就导致一个问题,不同的请求还是有可能会使用到同一个线程(只要请求数量大于线程数量),而ThreadLocal是属于线程的,如果我们使用完ThreadLocal对象而没有手动删掉,那么后面的请求就有机会使用到被使用过的ThreadLocal对象,如果一个请求在使用ThreadLocal的时候,是先get()来判断然后再set(),那就会有问题,因为get到的是别的请求set的内容,如果一个请求每次使用ThreadLocal,都是先set再get,那就不会有问题,因为一个线程同一时刻只被一个请求使用,只要我们每次使用之前,都设置成自己想要的内容,那就不会在使用的过程中被覆盖。使用ThreadLocal最好是每次使用完就调用remove方法,将其删掉,避免先get后set的情况导致业务的错误。