解读ThreadLocal

简单使用

 

kotlin

复制代码

class ThreadLocalTest { companion object { /** * 定义一个ThreadLocal变量 */ private val threadLocal = object : ThreadLocal<Int>() { /** * 重写该方法,指定一个初始值 */ override fun initialValue(): Int { return 1 } } @JvmStatic fun main(args: Array<String>) { /** * 开启子线程 */ thread(name = "threadLocal-1") { // 先获取旧值 val oldValue = threadLocal.get() // 设置新值 threadLocal.set(3) // 再获取新值 val newValue = threadLocal.get() // 打印线程名称,旧值,新值 println("thread name :${Thread.currentThread().name}| oldValue = $oldValue, newValue = $newValue") } /** * 开启子线程 */ thread(name = "threadLocal-2") { // 先获取旧值 val oldValue = threadLocal.get() // 设置新值 threadLocal.set(8) // 再获取新值 val newValue = threadLocal.get() // 打印线程名称,旧值,新值 println("thread name :${Thread.currentThread().name}| oldValue = $oldValue, newValue = $newValue") } } } }

运行结果

实现原理

类图

从图中我们可以看到,在我们的测试类ThreadLocalTest中,开启了两个线程,thread-1thread-2,分别在两个线程中调用我们创建好的threadLocal变量的setget方法,先分别看下setget的源码

set方法

 

java

复制代码

public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap成员变量 ThreadLocalMap map = getMap(t); if (map != null) // 如果能获取到,则直接设置值 map.set(this, value); else // 否则创建ThreadLocalMap对象再赋值 createMap(t, value); }

get方法

 

java

复制代码

public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap成员变量 ThreadLocalMap map = getMap(t); if (map != null) { // 获取map对象中对应的Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") // 取出key对应的值并返回 T result = (T)e.value; return result; } } // 否则创建ThreadLocalMap对象并返回initialValue()方法的值 return setInitialValue(); }

getMap()

 

java

复制代码

ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

createMap()

 

java

复制代码

void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

set方法的源码中我们可以看到,首先会获取到调用set方法时程序所在的线程,然后再拿到当前线程对应的ThreadLocalMap成员变量,如果不为空,再调用ThreadLocalMap成员变量对应的set方法,把当前ThreadLocal对象作为key,保存到ThreadLocalMap对象中,为空,则创建再保存相应的值,ThreadLocalMap对象会持有一个Entry类型的数组,Entry是一个类似于Map结构的类,继承于WeakReference,get方法也有类似的逻辑,不做详细分析。

结论

不难发现,我们在调用ThreadLocalset或者get方法时,内部实际上调用的是ThreadLocalMapset或者get方法,我们可以理解为ThreadLocal只是对set或者get方法做了一层封装,而ThreadLocal本身作为一个壳供外部使用,而ThreadLocalMap是线程持有的成员变量,因此,我们在多线程的场景中使用ThreadLocal来保存变量,实际上就是不同的线程的成员变量ThreadLocalMap对变量做了一个副本,对变量的值的改变也只是改变了当前线程的变量的值,不会影响到其他线程的变量的值的改变,起到了一个线程隔离的作用。

关系图

简单解释下,ThreadLocalMapThread中的成员变量,ThreadLocalMap中持有Entry对象,Entry是一个类似于Map的数据结构的类,ThreadLocal作为key,Object作为值被保存下来,Entry是一个数组,目的是为了在同一个线程中能够保存多种数据类型的变量副本。

内存泄漏问题

为什么会导致内存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些key为nullEntryvalue如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread -> ThreaLocalMap -> Entry -> value,永远无法回收,造成内存泄漏。 其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue。 但是这些被动的预防措施并不能保证不会内存泄漏

为什么ThreadLocal设计成弱引用

在源码当中,ThreadLocal被设计成了弱引用,目的是降低OOM的可能性,不会因此避免OOM,我们都知道,弱引用在JVM中,当系统进行一次GC时,会回收掉被标记为弱引用的对象,ThreadLocalMap的生命周期是跟Thread的生命周期一致的,如果Thread的生命周期足够长,Thread会一直持有ThreadLocalMap对象:

  • 如果是强引用, ThreadLocalMap也会一直持有ThreadLocal,作为keyThreadLocal也就会随着ThreadLocalMap的生命周期一直存在,得不到释放,慢慢累积,key会越积越多,最终导致内存泄漏。
  • 如果是弱引用,GC时,当对ThreadLocal的强引用被回收时,系统会自动回收掉这部分ThreadLocal,并且当我们在下一次调用setget或者remove方法时,也会去清除掉这部分ThreadLcoal,从而降低对内存的消耗,但是ThreadLocalMap依然还持有对Entry的引用,而Entryvalue也是强引用,因此如果不手动释放,也会造成内存泄漏

如何避免

既然知道了造成内存泄漏的原因,那我们也就能够对症下药了,就是在每次使用完ThreadLocal的时候调用remove()方法清除掉value就行了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值