ThreadLocal原理、使用场景及存在内存泄漏的原因

什么是线程封闭

当多线程访问共享变量时,往往需要加锁来保证共享变量的线程安全(数据同步)。一种避免使用加锁方式就是不共享数据,而是让线程独享数据。由于数据本身就是线程私有的,这样,如果仅在单线程内访问数据就不需要同步,这种避免共享数据的技术称为线程封闭。在Java语言中,提供了一些类库和机制来维护线程的封闭性,例如局部变量(存放在线程栈中)和ThreadLocal类,本文首先介绍ThreadLocal类保证线程封闭的原理,再讲一下如何使用ThreadLocal类来保证线程封闭。

以下内容全部来自 ThreadLocal的设计理念与作用 -《 Java基础 》一文,链接http://blog.qianxuefeng.com/article/153

ThreadLocal是什么

ThreadLocal是一个关于创建线程局部变量的类。通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。

如何创建ThreadLocal

我们可以看到,通过代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值。

创建ThreadLocal对象时,我们可以指定泛型,这样我们就不需要每次对使用get()方法返回的值作强制类型转换了;并且我们也可以设置初始值。

如何访问ThreadLocal变量

测试代码

从上述代码的运行结果,我们可以看出,两个线程的ThreadLocal值并未互相干扰。

实现原理

2.ThreadLocal源码中初始化、get、set都会通过最后的getMap、createMap两个方法获取ThreadLocalMap对象。通过这两个方法可以看出,最终存储的地方实际上是上述Thread源码中预留的threadLocals变量,而这个变量是线程实例化后每个对象独立的变量。

以上为ThreadLocal的设计理念与作用 -《 Java基础 》一文内容,这里是转载作者的内容,图片也是原作者的,这里尴尬打上了我的水印,还请原作者见谅。

使用场景

1.实现单个线程单例以及单个线程上下文信息存储,比如交易id等。

2.实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例。

3.承载一些线程相关的数据,避免在方法中来回传递参数。

ThreadLocal会导致内存泄露吗?

会。我们先看一下ThreadLocalMap类的结构及实现。

如上图ThreadLocalMap内部是一个Entry数组,Entry继承自WeakReference,Entry内部的key就是ThreadLocal本身,value是ThreadLocal的set方法传递的值(ThreadLocal的value)。ThreadLocal作为key被传递到了WeakReference的构造函数里面(super(k)),也就是说ThreadLocalMap里面的key为ThreadLocal对象的弱引用,value为具体调用ThreadLocal的set方法传递的值。

当一个线程调用ThreadLocal的set方法设置变量时候,当前线程的ThreadLocalMap(Thread类的ThreadLocal.ThreadLocalMap threadLocals成员变量)里面就会存放一个记录,这个记录的key为ThreadLocal的引用,value则为设置的值。如果当前线程一直存在而没有调用ThreadLocal的remove方法,并且这时候其它地方还是有对ThreadLocal的引用,则当前线程的ThreadLocalMap变量里存在的ThreadLocal变量的引用和value对象的引用是不会被释放的,这就会造成内存泄露的。但是考虑如果这个ThreadLocal变量没有了其他强引用,而当前线程还存在的情况下,由于线程的ThreadLocalMap里面的key是弱引用,则当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用会在gc的时候被回收,但是对应value还是会造成内存泄露,这时候ThreadLocalMap中就会出现key为null但是value不为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收(由于key为null无法访问到value,但是Entry还引用着value,无法对value进行GC),造成内存泄漏。

因此导致内存泄漏的原因是作为key的ThreadLocal为弱引用。

其实在ThreadLocal的set和get和remove方法里面有一些时机是会对这些key为null,value不为null的entry进行清理的,但是这些清理不是必须发生的。调用ThreadLocal的remove()方法会确保清理这些key为null的entry。下面看一下ThreadLocal的remove()方法:

会调用ThreadLocalMap的remove()方法:

e.clear()清除对ThreadLocal的弱引用

避免内存泄漏的方法是,必须调用ThreadLocal的remove()方法。

既然弱引用导致了内存泄漏,为什么还使用弱引用?


我们先来看看官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。


下面我们分两种情况讨论:
        key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal的引用不会被回收,导致内存泄漏(引用的对象被回收,但引用还在)。
        key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收(引用及引用的对象均被回收,保证ThreadLocal的引用被回收,是一个进步)。

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

避免内存泄漏的方法是,必须调用ThreadLocal的remove()方法。

 

猜你喜欢

转载自blog.csdn.net/qq_27127145/article/details/83868085
今日推荐