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()方法。

 

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocal是Java中的一个线程封闭对象,它提供了一种在多线程环境下实现线程私有变量的机制。每个线程都可以通过ThreadLocal对象获取自己的私有变量,并且线程之间互不干扰。这样可以方便地在多线程场景下共享数据,而又不用担心线程安全的问题。 ThreadLocal原理是通过在每个线程中维护一个ThreadLocalMap对象来存储线程私有变量。ThreadLocalMap是ThreadLocal类的一个静态内部类,它使用ThreadLocal对象作为key,实际的变量值作为value。每个线程对应一个ThreadLocalMap,通过ThreadLocal对象可以获取到当前线程对应的ThreadLocalMap。 当我们调用ThreadLocal的set方法来设置变量值时,实际上是将该值存储在当前线程的ThreadLocalMap中。当我们调用ThreadLocal的get方法来获取变量值时,实际上是从当前线程的ThreadLocalMap中查找对应的值。 至于内存泄露问题,ThreadLocal存在一定的潜在风险。如果我们在使用ThreadLocal后没有进行及时的清理操作,就有可能导致内存泄露。这是因为ThreadLocalMap中的Entry对象持有了对ThreadLocal对象的强引用,而线程的生命周期比较长,如果没有及时清理,那么即使线程已经结束了,ThreadLocal对象也无法被回收,从而导致内存泄露。 为了避免内存泄露,我们可以在使用ThreadLocal后调用remove方法来手动清理对应的Entry对象。另外,使用ThreadLocal时要特别注意避免在高并发场景下出现内存泄露的问题,可以合理地使用线程池,控制ThreadLocal使用范围,或者使用InheritableThreadLocal来代替ThreadLocal
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值