JAVA-DIY-12-WEEK

Persistence is not necessarily successful, but not sure will not succeed.

Posted by Hyuga on June 23, 2019

第12次讨论主题:ThreadLocal的问题根源

什么是ThreadLocal

java.lang.ThreadLocal由JDK1.2版本开始提供,是为解决多线程的并发问题而提供的一种新思路。

JDK1.5版本引入泛型,ThreadLocal升级为ThreadLocal.接口内对应方法也支持泛型。

使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量(线程局部变量:ThreadLocalVariable),主要用于将私有线程和该线程存放的副本对象做一个映射。

每个线程内嵌一个ThreadLocal局部变量,多线程并发场景下,各线程之间的变量互不干扰。

ThreadLocal的底层数据结构

此处内容来源:

文章标题:ThreadLocal的总结思考

文章作者:泡芙掠夺者

文章地址:https://www.jianshu.com/p/9c03c85db06e

  • ThreadLocal底层的数据模型是ThreadLocalMap,该Map是一个基于开放地址法实现的哈希表,key是当前的threadlocal对象,value是当前线程放在这个threadlocal里面的值;
  • 每个线程持有一个ThreadLocalMap对象A,A里面放的是和当前线程相关的threadlocal;
  • 如果认真看过源码,还会知道ThreadLocalMap的实现和WeakHashMap实现有异曲同工之妙,都通过WeakReference封装了key值,防止可怕的内存泄漏;
  • 再举个get()数据的例子。第一步得到当前线程;第二步获取当前线程对应的ThreadLocalMap;第三步以当前threadlocal对象为key,去ThreadLocalMap中把值捞出来;

作者【泡芙掠夺者】总结非常深入,推荐去原文阅读。


一个线程Thread,有个成员属性ThreadLocal.ThreadLocalMap threadLocals = null;

官方注释:与此线程相关的ThreadLocal值。这个映射由ThreadLocal类维护。

而ThreadLocal类有一个静态类ThreadLocalMap,ThreadLocalMap中有个Entry[]数组,Entry是一个key-value结构的对象。

Entry的key是经由WeakReference包装的ThreadLocal对象。

由ThreadLocal的数据结构和弱引用的特性可知,如果ThreadLocalMap中某个key【ThreadLocal】已经不用了,最终只会有一个WeakReference指向它,这个key在下次gc时就会被自动回收掉,不会一直停留在ThreadLocalMap中。

但是有个问题,key对应的ThreadLocal已经回收了,可是key对应的value并非弱引用,不会被gc所回收。

这种结构的好处:

  • 线程死去的时候,线程共享变量ThreadLocalMap则销毁;
  • ThreadLocalMap<ThreadLocal,Object>键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map<Thread, Object>键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。

ThreadLocal如何回收value,什么时候回收?

ThreadLocal接口只有4个方法,分别是:

  • void set(T value) 设置线程局部变量值
  • T get() 获取线程局部变量值
  • protected T initialValue() 获取线程局部变量的初始值,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
  • public void remove() 删除线程局部变量值

值得一提的是,remove()方法是JDK1.5才新增的方法,目的是为了减少内存的占用。需要重点关注最终调用的expungeStaleEntry()方法。

关于ThreadLocalMap<ThreadLocal, Object>弱引用问题:

当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。

虽然ThreadLocal的get,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用,所以为了防止此类情况的出现,有以下两种方式避免。

  • 使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
  • JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。
public class ThreadLocal<T> {
    ...
    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

结构剖析,ThreadLocal内部静态类ThreadLocalMap,并不是常规意义的map,而是利用弱引用实现了一层映射绑定。

map存储了弱引用类型ThreadLocal和强引用类型value是一层关联,当Thread执行完后,map这层引用关联中的弱引用类型ThreadLocal将会失效,但是强引用类型的value却不会被回收,而且也不会再被调用。

所以会存在内存泄露的风险。

最好是ThreadLocal获取完value后,明确后续不再需要使用value值,则手动调用remove()方法移除value.

实际上是触发了expungeStaleEntry()方法,将Entry[]中key为null的value给清理掉。


这里我们思考一个问题:ThreadLocal使用到了弱引用,是否意味着不会存在内存泄露呢?

首先来说,如果把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,因此GC是可以回收这部分空间的,也就是key是可以回收的。但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。

因此,只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。

那么如何有效的避免呢?

事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。我们也可以通过调用ThreadLocal的remove方法进行释放!

此处内容来自:

  • 作者:dawang325
  • 来源:CSDN
  • 原文:https://blog.csdn.net/u011213044/article/details/80581236

ThreadLocal为什么会产生脏数据?

产生脏数据的问题主要是出现在线程池中,由于线程池会复用一些现成的Thread对象,假如之前与该Thread对象绑定的静态属性ThreadLocal没被remove()掉,那么重用的这个线程对象,get出来的可能是上一个线程对象遗留的ThreadLocal局部变量值,搞不好就拿出来了上个线程对象的value值。