# ThreadLocal会内存泄露吗?

# 版本

JDK 1.8.0_231

# 前言

带着问题看

  1. ThreadLocal是什么?
  2. 怎么用?
  3. 为什么要用它?
  4. 它有什么缺点?
  5. 怎么避免内存泄露

# ThreadLocal是什么?

在了解ThreadLocal之前,我们先了解下什么是线程封闭

把对象封闭在一个线程里,即使这个对象不是线程安全的,也不会出现并发安全问题。

实现线程封闭大致有三种方式:

  • Ad-hoc线程封闭:维护线程封闭性的职责完全由程序来承担,不推荐使用
  • 栈封闭:就是用栈(stack)来保证线程安全。如下面代码所示,虽然StringBuilder是线程不安全的,但是它只是个局部变。局部变量存储在虚拟机栈,虚拟机栈是线程隔离的,所以不会有线程安全问题
public void testThread() {
    StringBuilder sb = new StringBuilder();
    sb.append("Hello");
}
  • ThreadLocal线程封闭:通过ThreadLocal来实现线程之间的数据隔离,简单易用

# 使用

JDK1.2开始提供的java.lang.ThreadLocal的使用方式非常简单

public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {

        final ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("main-thread : Hello");

        Thread thread = new Thread(() -> {
            // 获取不到主线程设置的值,所以为null
            System.out.println(threadLocal.get());
            threadLocal.set("sub-thread : World");
            System.out.println(threadLocal.get());
        });
        // 启动子线程
        thread.start();
        // 让子线程先执行完成,再继续执行主线
        thread.join();
        // 获取到的是主线程设置的值,而不是子线程设置的
        System.out.println(threadLocal.get());
        threadLocal.remove();
        System.out.println(threadLocal.get());
    }
}

运行结果

null
sub-thread : World
main-thread : Hello
null

运行结果说明了ThreadLocal只能获取本线程设置的值,也就是线程封闭。基本上,ThreadLocal对外提供的方法只有三个get()、set(T)、remove()。

# 原理

使用方式非常简单,所以我们来看看ThreadLocal的源码。ThreadLocal内部定义了一个静态ThreadLocalMap类,ThreadLocalMap内部又定义了一个Entry类,这里只看一些主要的属性和方法

public class ThreadLocal<T> {

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }

    // 从这里可以看出ThreadLocalMap对象是被Thread类持有的
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    // 内部类ThreadLocalMap
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            // 内部类Entity,实际存储数据的地方
            // Entry的key是ThreadLocal对象,不是当前线程ID或者名称
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        // 注意这里维护的是Entry数组
        private Entry[] table;
    }
}

根据上面的源码,可以大致画出ThreadLocal在虚拟机内存中的结构

ThreadLocal大概结构

实线箭头表示强引用,虚线箭头表示弱引用。

强引用(StrongReference): 当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收
软引用(SoftReference): 当系统内存空间足够时,它不会被系统回收,程序也可以使用该对象;当系统内存空间不足时,系统可能会回收它。
弱引用(WeakReference): 当系统垃圾回收机制运行时,无论系统内存是否足够,总会回收该对象所占用的内存
虚引用(PhantomReference): 若是一个对象只有一个虚引用时,那么它和没有引用的效果大体相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。

需要注意的是:

  • ThreadLocalMap虽然是在ThreadLocal类中定义的,但是实际上被Thread持有。
  • Entry的key是(虚引用的)ThreadLocal对象,而不是当前线程ID或者线程名称。
  • ThreadLocalMap中持有的是Entry数组,而不是Entry对象。

对于第一点,ThreadLocalMap被Thread持有是为了实现每个线程都有自己独立的ThreadLocalMap对象,以此为基础,做到线程隔离。第二点和第三点理解,我们先来想一个问题,如果同一个线程中定义了多个ThreadLocal对象,内存结构应该是怎样的?此时再来看一下ThreadLocal.set(T)方法:

public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 根据线程对象获取ThreadLocalMap对象(ThreadLocalMap被Thread持有)
    ThreadLocalMap map = getMap(t);
    // 如果ThreadLocalMap存在,则直接插入;不存在,则新建ThreadLocalMap
    if (map != null)
        map.set(this,value);
    else
        createMap(t,value);
}

也就是说,如果程序定义了多个ThreadLocal,会共用一个ThreadLocalMap对象,所以内存结构应该是这样

ThreadLocal-同一线程中多个ThreadLocal的关系

这个内存结构图解释了第二点和第三点。假设Entry中key为当前线程ID或者名称的话,那么程序中定义多个ThreadLocal对象时,Entry数组中的所有Entry的key都一样(或者说只能存一个value)。ThreadLocalMap中持有的是Entry数组,而不是Entry,则是因为程序可定义多个ThreadLocal对象,自然需要一个数组。

# ThreadLocal会发生内存泄漏吗?

仔细看下ThreadLocal内存结构就会发现,Entry数组对象通过ThreadLocalMap最终被Thread持有,并且是强引用。也就是说Entry数组对象的生命周期和当前线程一样。即使ThreadLocal对象被回收了,Entry数组对象也不一定被回收,这样就有可能发生内存泄漏。

# ThreadLocal在设计的时候就提供的一些补救措施

  • Entry的key是弱引用的ThreadLocal对象,很容易被回收,导致key为null,但是value不为null的情况。在调用Entry的get()、set(T)、remove()等方法的时候,会自动清理key为null的Entry。
  • remove()方法主要是用来清理ThreadLocalMap中key为当前ThreadLocal的Entry,同时也会自动清理key为null的Entry。

有些文章认为是弱引用导致了内存泄漏,其实是不对的。假设把弱引用变成强引用,这样无用的对象key和value都不为null,反而不利于GC,只能通过remove()方法手动清理,或者等待线程结束生命周期。也就是说ThreadLocalMap的生命周期由持有它的线程来决定,线程如果不进入terminated状态,ThreadLocalMap就不会被GC回收,这才是ThreadLocal内存泄露的原因。

# 既然Entry的key(ThreadLocal)是弱引用,很容易被垃圾回收,在使用上会有问题吗?

不会

虽然Entry对ThreadLocal是弱引用,ThreadLocalReference对ThreadLocal是强引用,当ThreadLocalReference还存在时,ThreadLocal是不会被垃圾回收的

# 与synchronized的关系

有些文章拿ThreadLocal和synchronized比较,其实它们的实现思想不一样。

  • synchronized是同一时间最多只有一个线程执行,所以变量只需要存一份,算是一种时间换空间的思想
  • ThreadLocal是多个线程互不影响,所以每个线程存一份变量,算是一种空间换时间的思想

# 应用场景

  • 维护JDBC的java.sql.Connection对象,因为每个线程都需要保持特定的Connection对象。
  • Web开发时,有些信息需要从controller传到service传到dao,甚至传到util类。看起来非常不优雅,这时便可以使用ThreadLocal来优雅的实现。
  • 包括线程不安全的工具类,比如Random、SimpleDateFormat等

# 总结

ThreadLocal是一种隔离的思想,当一个变量需要进行线程隔离时,就可以考虑使用ThreadLocal来优雅的实现。

# 最佳实践

  1. 将ThreadLocal变量定义成private static的,保证ThreadLocal的生命周期可控。由于一直存在ThreadLocal的强引用,ThreadLocal不会被垃圾回收,保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值
  2. 每次使用完ThreadLocal,都调用remove()方法,清除数据,避免内存泄露
Last Updated: 6/30/2022, 5:29:06 PM