ThreadLocal源码详解及内存泄漏原理

 ThreadLocal的作用

官方定义:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

此类提供线程局部变量。这些变量与普通对应变量的不同之处在于,访问一个变量的每个线程(通过其 get 或 set 方法)都有自己独立初始化的变量副本。ThreadLocal 实例通常是希望将状态与线程(例如,用户 ID 或事务 ID)相关联的类中的私有静态字段。

ThreadLocal为每个线程都提供了变量的副本, 使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

由此也可以看出ThreadLocal和Synchonized都用于解决多线程并发访问。

可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。ThreadLocal则是副本机制。此时不论多少线程并发访问都是线程安全的。

ThreadLocal的一大应用场景就是跨方法进行参数传递,

1.Web容器中,每个完整的请求周期会由一个线程来处理。结合ThreadLocal再使用Spring里的IOC和AOP,就可以很好的解决我们上面的事务的问题。

2.将一个数据库连接放入ThreadLocal中,当前线程执行时只要有使用数据库连接的地方就从ThreadLocal获得就行了。

3.在微服务领域,链路跟踪中的traceId传递也是利用了ThreadLocal。

ThreadLocal的使用

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

java复制代码/*设置当前线程的线程局部变量的值。*/
• void set(Object value)
/*该方法返回当前线程所对应的线程局部变量。*/
• public Object get()
/*前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。*/
• public void remove()
/*返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。*/
• protected Object initialValue()

ThreadLocal实现解析

实现分析

怎么实现ThreadLocal,既然说让每个线程都拥有自己变量的副本,最容易的方式就是用一个Map将线程的副本存放起来,Map里key就是每个线程的唯一性标识,比如线程ID。value就是副本值,实现起来也很简单。

DougLee在《并发编程实战》中为我们做过性能测试:

可以看到ThreadLocal的性能远超类似synchronize的锁实现ReentrantLock,比我们后面要学的AtomicInteger也要快很多,即使我们把Map的实现更换为Java中专为并发设计的ConcurrentHashMap也不太可能达到这么高的性能。

怎么样设计可以让ThreadLocal达到这么高的性能呢?

最好的办法则是让变量副本跟随着线程本身,而不是将变量副本放在一个地方保存,这样就可以在存取时避开线程之间的竞争。

同时,因为每个线程所拥有的变量的副本数是不定的,有些线程可能有一个,有些线程可能有2个甚至更多,则线程内部存放变量副本需要一个容器,而且容器要支持快速存取,所以在每个线程内部都可以持有一个Map来支持多个变量副本,这个Map被称为ThreadLocalMap。

具体实现图解

源码解析

类的定义

首先Thread类里定义了一个ThreadLocalMap类型的成员变量存储ThreadLocal对象和线程副本值。

ThreadLocal.ThreadLocalMap threadLocals = null;

而ThreadLocalMap类是定义在ThreadLocal类内部的静态内部类。

而它内部定义了一个Entry类存储每个键值对(ThreadLocal对象和线程副本值) (与Hashmap不同的是它处理哈希冲突使用的是开发定址法的线性探测再散列)

Entry类继承了WeakReference,构造方法里调用父类WeakReference构造方法传入的是ThreadLocal对象。所以ThreadLocal对象是一个弱引用对象。(关于弱引用可以看我的这篇博客juejin.cn/post/725067…)

ThreadLocal.set()

java复制代码public void set(T value) {
    Thread t = Thread.currentThread();
    获取当前线程的map对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //map已经初始化,调用map的set方法,存入map中,key即为ThreadLocal对象,value为副本值
        map.set(this, value);
    else
        //还未初始化,则初始化map并存入当前键值对
        createMap(t, value);
}

getMap方法直接返回当前线程的ThreadLocalMap类的对象threadLocals

java复制代码ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

ThreadLocalMap.set():构造一个新Entry对象,根据hash算法,存到map中。

java复制代码private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocal.get()

java复制代码public T get() {
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //若map不为空
    if (map != null) {
        //根据当前ThreadLocal对象为key查询value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            //得到的Entry对象不为空,返回value值
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //若map为空,直接初始化map
    return setInitialValue();
}

ThreadLocalMap.getEntry():根据key值获取对应的副本值

java复制代码private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

ThreadLocal造成的内存泄露

根据我们前面对ThreadLocal的分析,我们可以知道每个Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

仔细观察ThreadLocalMap,这个map是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

因此使用了ThreadLocal后,引用链如图所示:图中的虚线表示弱引用。

这样,当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal实例将会被gc回收。 这样一来ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块value永远不会被访问到了,所以存在着内存泄露。

只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是在不需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。

从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

下面我们分两种情况讨论:

  • key 使用强引用:对ThreadLocal对象实例的引用被置为null了,但是ThreadLocalMap还持有这个ThreadLocal对象实例的强引用,如果没有手动删除,ThreadLocal的对象实例不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:对ThreadLocal对象实例的引用被被置为null了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal的对象实例也会被回收。value在下一次ThreadLocalMap调用set,get,remove都有机会被回收。

ps:在执行ThreadLocal的set、get、remove方法的时候会去遍历Entry数组,如果发现key为空则会干掉该Entry对象的value值,所以设计成弱引用是为了多一层保障

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。

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

总结

  • JVM利用设置ThreadLocalMap的Key为弱引用,来尽量避免内存泄露。
  • JVM利用调用remove、get、set方法的时候,回收弱引用。
  • 当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
  • 使用线程池+ ThreadLocal时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值