前言
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
就会存在一条强引用链,永远无法回收,造成内存泄漏。
其实ThreadLocal
的设计中已经考虑到了这种情况,也加上了一些预防措施,在调用get
、set
、remove
方法的时候,会清楚线程ThreadLocalMap
里所有key
为null
的value
。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
- 分配使用了ThreadLocal又不再调用get() ,set() ,remove() 方法,那么就会导致内存泄漏。
为什么使用弱引用
从表面上看内存泄漏的根本原因是使用了弱引用,那么为什么使用弱引用而不使用强引用呢?下面看看官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
翻译过来就是:为了应对非常大和长时间的用途,哈希表使用弱引用。
下面我们分两种情况讨论:
- key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
- key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set , get , remove 的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal最佳实践
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
- 每次使用完ThreadLocal,都调用它的remove() 方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。