死磕ThreadLocal,为何ThreadLocal实现如此复杂,直接封装HashMap不香吗? 您所在的位置:网站首页 threadlocal为什么不用hashmap 死磕ThreadLocal,为何ThreadLocal实现如此复杂,直接封装HashMap不香吗?

死磕ThreadLocal,为何ThreadLocal实现如此复杂,直接封装HashMap不香吗?

2024-07-17 14:01| 来源: 网络整理| 查看: 265

你以为你懂了,其实你没有!  ——写给自己

 

一直以来认为ThreadLocal只是简单的分装了一下HashMap,使用线程作为key来存储。这样也符合我们的习惯思维。需要存储多少线程变量就创建多少ThreadLocal。

及如下图这样:

package gy.mao; import java.util.HashMap; import java.util.Map; public class MyThreadLocal { Map map = null; public MyThreadLocal() { } public void set(T value) { if (map == null) { map = new HashMap(); } map.put(Thread.currentThread(), value); } public T get(){ if (map == null) { throw new RuntimeException("还未初始化"); } return map.get(Thread.currentThread()); } public int size(){ if (map == null) { throw new RuntimeException("还未初始化"); } return map.size(); } }

及通过对HashMap实现简单的封装就直接使用;

然而今天仔细的看了看却发现他实际是这样的

 

问题一、为什么实际的ThreadLocal却不是我们通常所理解的这样呢?

这就涉及到我们常说的内存泄露的问题。

通过简单封装HashMap的方式,如果不手动remove()线程所放入的Entry。那么由于ThreadLocal一直存在,导致该Entry将一直持有,不会被垃圾回收器回收。随着线程不断的创建,put数据,必然会内存溢出。

 

问题二、可是不都说使用的ThreadLocal也是需要remove() 才行,要不然也会内存溢出啊

在回答这个问题前,先做个测试

首先是使用我自己实现的MyThreadLocal测试

然后是使用原生的ThreadLocal测试

两个程序实现一样的功能,都是不断的创建线程然后往ThreadLocal中存数据。

可以很明显的观察到,使用我自己实现的MyThreadLocal最终会导致内存溢出,并且手动触发垃圾回收也无济于事;这也很符合我们的预期。

可是使用原生的ThreadLocal及时在没有使用remove的情况下,也没有内存溢出,而且垃圾回收还很规律的在进行。

 

问题三、ThreadLocal不使用remove()会有内存泄露的问题?

一起来简单看下源码,set()方法,

首先是调用了getMap方法及通过当前线程去获取ThreadLocalMap(再深入可以看到该ThreadLocalMap实际是属于当前线程所有),

没有则创建,有则直接set值进去(注意此时的set的值为this,及当前ThreadLocal)。

再来看下ThreaLocalMap,可以很明显的看到里面的Entry继承了弱引用

及ThreadLocal的真正的实现是这样的(图片来源于网络)

 

 

 

因此可以看出,真正的ThreadLocal的哲学为:一个线程维护一个ThreaLocalMap,多个线程变量ThreadLocal存放在这个ThreadLocalMap中,ThreadLocal以弱引用的方式作为ThreadLocalMap的key。

所谓的不remove()会导致内存溢出是: 由于ThreadLocalMap的Entry中key是弱引用,当ThreadLocal不再被使用及会被垃圾回收器回收,但是value 由于是被Entry强引用,而Entry又是被ThreadLocalMap强引用,而ThreadLocalMap又是属于当前线程的。这样就形成key已经被回收了,但是Value一直不能回收,且不会再次使用到。

 

问题四、前面的实验中使用ThreadLocal没有导致内存泄露的原因是?

仔细看代码就知道,代码中每次都是创建线程之后就往ThreadLocal中放数据,而放完数据之后线程马上就结束了。因此对应的ThreadLocal,依附于Thread的ThreadLocalMap都是可以被垃圾回收的,及皮将不存,毛之焉附。而不remove会到导致内存泄露是线程还在继续运行的情况。

 

问题五、为啥ThreadLocalMap的key是弱引用而不是正常的强引用呢?

通过上面的源码分析,我门已经知道ThreadLocalMap是数据线程对象的,而如果ThreadLocalMap的key为强引用,则当线程运行结束回收ThreadLocalMap对象时,由于存在ThreadLocal对ThreadLocalMap对象的强应用将导致ThreadLocalMap对象无法被回收,及线程已经结束,但线程对象ThreadLocalMap却没有办法被回收,造成内存泄漏。

 

此外,ThreadLocal没有采用我锁实现的MyThreadLocal这种实现思想的一个重要原因是锁的问题。

众所周知HashMap有线程安全的问题,如果要使用MyThreadLocal的这种思想必然得考虑该问题,得使用锁来控制,这无疑增加了性能损耗和复杂度。

而反观系统提供的ThreadLocal,由于是一个线程维护一个ThreadLocalMap数据,从设计上已经避免了多线程带来的安全问题。

 

总结

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法 每次使用完ThreadLocal都调用它的remove()方法清除数据 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

参考:https://zhuanlan.zhihu.com/p/102571059

 



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有