最近复习到ThreadLocal
,提到ThreadLocal
,难免要提一下它可能的内存泄漏问题(就像提到CAS
就不得不提ABA
问题),就难免要提一下为什么会造成内存泄露呀,怎么避免呀。懂的同学都知道内存泄漏是因为key
是弱引用,避免的方法是在用完ThreadLocal
之后调用一下它的remove()
。但是为什么key
要用弱引用呢,直接用强引用,value
不就不会泄露了吗?避免这个内存泄漏真的像说的那么简单吗?还会有其它问题吗?
1 为什么key要用弱引用
我在学ThreadLocal
的时候就在想,为啥要定义个ThreadLocalMap
呢,还要写它的散列方法,还要写那么多其它方法,key
还要用弱引用,还有可能造成value
的泄露,直接用HashMap
不香吗?
那么要用HashMap
来替代ThreadLocalMap
可不可以呢?我认为当然可以。只是它可能会造成更严重的内存泄露。我们来解释一下,假设:
- 在主线程中定义了一个
ThreadLocal<Integer>
的对象threadLocal
。 - 在线程1中用到了这个
threadLocal
,比如我们把它设为100,现在线程1的HashMap
中多了一个键值对threadLocal-100
. - 在主线程中将
threadLocal=null
。
现在造成了什么后果呢:
- 在主线程中将
threadLocal=null
,本意是让threadLocal
被GC,但是由于现在线程1中保留了此对象的引用,导致其没有被GC。 - 在线程1中也无法通过
threadLocal
这个引用访问到这个对象了,因为它被主线程置为了null
,现在再使用会导致空指针异常。 - 或许有同学说可以通过遍历
hashMap
来清理这个对象,但是这也是行不通的,原因是- 什么时候去遍历
hashMap
呢,你没事写着写着代码会想到遍历一次这个hashMap
吗? - 即使你遍历了,你能知道哪个(哪些)
ThreadLocal
对象已经没用了吗?显然这是十分困难的(如果你已经知道了那个ThreadLocal
对象没用了,那在他没用的一刹那你就可以把他从hashMap
中赶出去,何必等到现在;现在你只能看到一个个长得很像的ThreadLocal
对象,它们又没有名字,你怎么知道哪个是哪个呢)。
- 什么时候去遍历
- 还有一个办法,就是等待回收线程时回收
hashMap
,那么其中的内容自然就被清空。这听起来还算靠谱,但是如果是用线程池来管理线程,核心线程可能永远不会被回收,并且随着核心线程被反复使用,泄露的内存会越来越多。
这样会导致ThreadLocal
对象和其对应的value
都泄露,所以,看起来还是使用弱引用好一点,虽然可能造成value
内存泄漏,但毕竟两害相权取其轻嘛。
2 使用弱引用有什么问题
一句话概括就是:在key
使用弱引用的情况下,如果threadLocal
被回收了,那么就无法通过threadLocal
访问到其对应的value
,value
就内存泄露了。
通常给出的解决的办法是在用完ThreadLocal
之后手动调用其remove()
方法。WTF?
- 内存泄漏是由于
ThreadLocal
被GC引起的,我不可能在它被GC之后调用它的remove()
,也就是说,内存泄漏一旦发生,这个办法也无法解决,这个办法只能说预防内存泄漏。 - 什么时候是
ThreadLocal
用完的时候?在多人开发的情况下你用完了他也用完了吗?今天写的代码在这里用完了明天代码改了还在这里用完吗? - 如果我知道在哪里用完了,我完全可以用
hashMap
呀,我用完了就把它从hashMap
里remove
掉不就行了。如果这个办法也算一个办法的话,那么就完全没有必要用弱引用了。
综上,在用完ThreadLocal
之后手动调用其remove()方法不能完全避免内存泄漏,只能预防。并且,如果确定什么时候用完ThreadLocal
这么简单的话,那就没有必要用弱引用了,用HashMap
就能解决问题。
其实如果ThreadLocal
类能提供一个静态方法来清理ThreadLocalMap
中无效的键值对还是不错的,这样就不用依赖对象的remove()
方法,让使用者在某个确认ThreadLocal
已经用完的地方(比如run()
方法最后)调一下,可惜我翻了一下没有这样的方法。ThreadLocalMap
这个类中倒是有一个cleanSomeSlots()
,但是它是私有方法。
3 还有什么问题
请循其本,我们一开始说到ThreadLocal
内存泄露的原因,是由于ThreadLocal
对象的引用被置为null
,然后ThreadLocal
对象被GC
了,导致无法通过key
访问到ThreadLocalMap
中对应的value
,继而导致value
的内存泄漏。
按照这个设定,在一个线程还没有用完ThreadLocal
对象之前,主线程就把它GC了,这是有可能的。这不明摆着坑线程嘛,你答应借给别人用,别人还没用好,你就把它回收了。这是我们编程本来就应该避免的,即使是其它任何类型的共享变量,在其它线程用完之前,主线程将其置为null
也有可能导致其它线程空指针异常。
所以主线程至少应该等待其他线程用完ThreadLocal
之后才将其回收。但话又说话来,如果知道其他线程什么时候用完,那么在用完的时候清理一下map
不就没内存泄漏问题了么。这又是一个矛盾的话题。
4 总结
这篇写的比较乱,没有一个完整的逻辑,只能算是一个钻牛角尖的思考过程吧,但我觉得对我还是很有用的。说一个我现在想到的防止内存泄露的办法吧。
- 在某个线程中用到
ThreadLocal
对象的时候,先自己重新定义一个它的引用,这虽然可能会导致ThreadLocal
对象不能及时被GC,但可以防止后面要调用remove()
方法的时候调用不到。 - 在确定自己用完这个
ThreadLocal
的时候,用自己定义的引用调用remove()
方法,然后将这个引用置为null
。这样ThreadLocal
对象和value
对象就都不会泄露了。
但是这个方法还是要自己确定什么时候用完ThreadLocal
,所以说到底这还是个伪办法,原因上面已经说了,如果能确定什么时候用完的话,用hashMap
就可以解决问题。但我觉得比原来的方法强一点吧,至少不会出空指针异常了。