Gear with Code Java Engineer

ThreadLocal思考二三事

2019-12-28

最近复习到ThreadLocal,提到ThreadLocal,难免要提一下它可能的内存泄漏问题(就像提到CAS就不得不提ABA问题),就难免要提一下为什么会造成内存泄露呀,怎么避免呀。懂的同学都知道内存泄漏是因为key是弱引用,避免的方法是在用完ThreadLocal之后调用一下它的remove()。但是为什么key要用弱引用呢,直接用强引用,value不就不会泄露了吗?避免这个内存泄漏真的像说的那么简单吗?还会有其它问题吗?

1 为什么key要用弱引用

我在学ThreadLocal的时候就在想,为啥要定义个ThreadLocalMap呢,还要写它的散列方法,还要写那么多其它方法,key还要用弱引用,还有可能造成value的泄露,直接用HashMap不香吗?

那么要用HashMap来替代ThreadLocalMap可不可以呢?我认为当然可以。只是它可能会造成更严重的内存泄露。我们来解释一下,假设:

  1. 在主线程中定义了一个ThreadLocal<Integer>的对象threadLocal
  2. 在线程1中用到了这个threadLocal,比如我们把它设为100,现在线程1的HashMap中多了一个键值对threadLocal-100.
  3. 在主线程中将threadLocal=null

现在造成了什么后果呢:

  1. 在主线程中将threadLocal=null,本意是让threadLocal被GC,但是由于现在线程1中保留了此对象的引用,导致其没有被GC。
  2. 在线程1中也无法通过threadLocal这个引用访问到这个对象了,因为它被主线程置为了null,现在再使用会导致空指针异常。
  3. 或许有同学说可以通过遍历hashMap来清理这个对象,但是这也是行不通的,原因是
    • 什么时候去遍历hashMap呢,你没事写着写着代码会想到遍历一次这个hashMap吗?
    • 即使你遍历了,你能知道哪个(哪些)ThreadLocal对象已经没用了吗?显然这是十分困难的(如果你已经知道了那个ThreadLocal对象没用了,那在他没用的一刹那你就可以把他从hashMap中赶出去,何必等到现在;现在你只能看到一个个长得很像的ThreadLocal对象,它们又没有名字,你怎么知道哪个是哪个呢)。
  4. 还有一个办法,就是等待回收线程时回收hashMap,那么其中的内容自然就被清空。这听起来还算靠谱,但是如果是用线程池来管理线程,核心线程可能永远不会被回收,并且随着核心线程被反复使用,泄露的内存会越来越多。

这样会导致ThreadLocal对象和其对应的value都泄露,所以,看起来还是使用弱引用好一点,虽然可能造成value内存泄漏,但毕竟两害相权取其轻嘛。

2 使用弱引用有什么问题

一句话概括就是:在key使用弱引用的情况下,如果threadLocal被回收了,那么就无法通过threadLocal访问到其对应的valuevalue就内存泄露了。

通常给出的解决的办法是在用完ThreadLocal之后手动调用其remove()方法。WTF?

  1. 内存泄漏是由于ThreadLocal被GC引起的,我不可能在它被GC之后调用它的remove(),也就是说,内存泄漏一旦发生,这个办法也无法解决,这个办法只能说预防内存泄漏。
  2. 什么时候是ThreadLocal用完的时候?在多人开发的情况下你用完了他也用完了吗?今天写的代码在这里用完了明天代码改了还在这里用完吗?
  3. 如果我知道在哪里用完了,我完全可以用hashMap呀,我用完了就把它从hashMapremove掉不就行了。如果这个办法也算一个办法的话,那么就完全没有必要用弱引用了。

综上,在用完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 总结

这篇写的比较乱,没有一个完整的逻辑,只能算是一个钻牛角尖的思考过程吧,但我觉得对我还是很有用的。说一个我现在想到的防止内存泄露的办法吧。

  1. 在某个线程中用到ThreadLocal对象的时候,先自己重新定义一个它的引用,这虽然可能会导致ThreadLocal对象不能及时被GC,但可以防止后面要调用remove()方法的时候调用不到。
  2. 在确定自己用完这个ThreadLocal的时候,用自己定义的引用调用remove()方法,然后将这个引用置为null。这样ThreadLocal对象和value对象就都不会泄露了。

但是这个方法还是要自己确定什么时候用完ThreadLocal,所以说到底这还是个伪办法,原因上面已经说了,如果能确定什么时候用完的话,用hashMap就可以解决问题。但我觉得比原来的方法强一点吧,至少不会出空指针异常了。


Comments

Content