详解ThreadLocal

ThreadLocal 线程本地变量

ThreadLocal被定义在jdk的<java.lang>包下面,他的作用在于辅助开发者操作线程本地变量,让我们可以在繁琐复杂的方法调用链中灵活的获取线程本地变量,而不用通过鸡肋的引用传递来获取N层调用次数之前的栈内变量。那为什么说他是辅助呢?因为真正的线程本地变量是通过ThreadLocalMap来储存的,而ThreadLocal封装了从当前线程中获取ThreadLocalMap的方法,我们可以通过get()、set(T)方法方便的对当前线程的栈内变量进行操作。这里有一点需要注意一下,ThreadLocal只具备操作当前线程的本地变量能力,并不能从当前线程直接获取其他线程的本地变量,而这一特性也经常被用来实现线程隔离,例如请求上下文、session获取等等。下面我们就来详细的分析一下ThreadLocal的原理及用法。

原理

在认识ThreadLocal之前,我们需要掌握一个前提,那就是Thread是怎么实现本地变量存储的。首先,jdk在Thread中定义了一个变量ThreadLocalMap用于存储线程本地变量,而这个ThreadLocalMap是一个自定义的HashMap结构,他的元素Entry同时也继承了WeakReference是一个弱引用,这个弱引用引用的对象就是Entry的Key即ThreadLocal对象,而Entry的值value是个强引用的对象。当我们需要将对象存入线程本地时,调用ThreadLocal#set(T)方法,ThreadLocal会取出当前线程的本地变量表ThreadLocalMap,将当前ThreadLocal和T以K-V形式存入这个Map中,至此便完成了存储过程;当我们在想取出时,只需要调用ThreadLocal#get()方法,取出线程的本地变量表ThreadLocalMap,以相同的算法计算hashkey,取出Value即可。

ThreadLocal的引用关系:
详解ThreadLocal

ThreadLocal.set(T)流程

详解ThreadLocal

ThreadLocal.get()流程

详解ThreadLocal

为什么使用弱引用?

这么设计的目的在于,当ThreadLocal这个对象没有任何强引用和软引用指向它的时候,代表我们在程序中将不会以任何一种方式可触达的方式主动获取到这个ThreadLocal,也就是说我们再也用不到这个Key所指向的ThreadLocalMap.Entry了,那么垃圾收集器在扫描到它的时候会尽快回收这个ThreadLocal的内存地址,从而避免造成内存泄漏。

弱引用的结构设计有什么不足?

弱引用的设计解决了ThreadLocalMap.Entry的key,即ThreadLocal的内存泄漏,但是,这样做真的就万无一失了吗?细心的同学应该已经发现了问题,Key的内存泄露就解决了,那么Value呢?Value在Entry是个强引用类型,既然线程还没有终止,那么ThreadLocalMap的引用就是有效的,他的Entry就不会被清理,这样内存中保存在Value的引用,但是我们再也不会获取到这块内存,这样不就造成了Value的内存泄漏吗?没错!ThreadLocal和Value都只是Entry中的引用,ThreadLocal的弱引用关系被清理后可以回收ThreadLocal的内存,但是由于Entry仍然被Map所引用,导致Value无法被回收,进而造成Value的内存泄露。

详解ThreadLocal

Value内存泄漏发生的条件
  • 持有ThreadLocalMap的线程处于存活状态

  • ThreadLocal的所有引用被显示取消,包括当前线程、其他线程、静态变量等。

    常见的场景有:

    • 在使用了线程池中的某个核心线程提交了任务以后,在任务中存储了本地变量并使用,当任务结束后并没有显示的调用remove,而核心线程是长期存活的,不会被清理,从而导致内存泄漏。
    • 在代码中使用了静态变量ThreadLocal用于在整个项目中使用,在执行某个任务时不小心将ThreadLocal置空,从而导致存活的线程Value获取不到且无法被释放。
如何避免Value内存泄漏?

在使用完线程本地变量后,一定要显示的调用ThreadLocal#remove()方法。在remove方法中,ThreadLocal会获取到ThreadLocalMap调用其remove方法,从而清除Entry、key、value的所有引用关系,GC收集器扫描到后会进行内存回收,避免泄漏。

上一篇:java声明全局变量的关键字,详细解说


下一篇:spring bean解决单例是并发不安全的问题