Heycm

Heycm

ThreadLocal 内存泄漏

2024-11-01
ThreadLocal 内存泄漏

    ThreadLocal 是一个实现线程本地存储数据的工具,每个线程保存独立的数据副本,避免线程之间发生数据共享。ThreadLocal 的内存泄漏原因点主要在于 ThreadLocalMap 的 key 是弱引用,以及 GC 对弱引用的回收机制。

工作原理

    了解 ThreadLocal 的工作原理,先看看关键源码

public class ThreadLocal<T> {


   /**
     * 取值,从当前线程的属性 threadLocals 中取值
     */
   public T get() {
        ThreadLocalMap map = Thread.currentThread().threadLocals;
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    /**
     * 写值,写入当前线程的属性 threadLocals 中
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;
        if (map != null)
            map.set(this, value);
        else
            t.threadLocals = new ThreadLocalMap(this, value)
    }
    

    /**
     * 线程存储本地数据的Map结构, 键为 ThreadLocal 对象本身, 且标记为弱引用, 值为需要存储的数据
     */
    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            
            Object value;

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

ThreadLocalMap 是存储数据的结构,在 ThreadLocal 中定义,但实际创建对象是作为 Thread 的一个属性

public class Thread implements Runnable {

    /** 
     * ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. 
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

}

它们的关系是

Thread -> ThreadLocalMap -> (ThreadLocal, 存储数据)

画个图辅助理解

64A4D879BD184C68A1EF5FD670A19A3A.png

GC 对 弱引用 的回收机制

    在发生 GC 时,只要发现被弱引用修饰的对象没有其他任何强引用指向,就会将该对象回收,不论内存空间是否有空闲。

内存泄漏原因

    这意味着,没有强引用指向 ThreadLocal 对象时,ThreadLocal 对象就会被回收,在 ThreadLocalMap 中体现为

Thread -> ThreadLocalMap -> (null, 存储数据)

这样,我们无法再访问到 存储数据 这一块对象,又因为存在 ThreadLocalMap.Entry 的引用指向,导致无法被 GC 回收,即发生内存泄漏,泄漏的内存为 存储数据 占用的内存空间,即 ThreadLocalMap 中的 Value。

直到该线程生命终止,ThreadLocalMap 一整个被回收掉。

public class Example {
    public static void main(String[] args) {
        ThreadLocal<String> local = new ThreadLocal<>();
        local.set("存储数据");

        // 清除强引用
        local = null;
        
        // 若此时发生 GC,ThreadLocal 实例会被回收
        // System.gc();
    }
}

内存泄漏场景

    在一般项目中,我们不会持续创建线程销毁线程,因为系统资源开销高,通常使用线程池。比如,Tomcat 连接池、一般业务线程池等等,线程池中的线程不会因为任务完成而终止,这意味着线程会在整个应用的生命周期内持续运行。

    假设线程池维持 200 个线程,每个线程维护一个 ThreadLocalMap,若应用中使用 10 个 ThreadLocal 对象,ThreadLocal 存储数据大小 1 MB,则有可能造成

    200 * 10 * 1 MB = 2000 MB

大小的内存泄漏。

安全使用

    一般的解决方案就是在一次使用完成后及时清除数据,避免残留在 ThreadLocalMap 中

public class Example {

    private ThreadLocal<String> local = new ThreadLocal<>();

    public void example() {
        try {
            local.set("存储数据");
            // do others...
        } finally {
            local.remove();
        }
    }
}

使用场景

    在 SpringBoot 构建的 Web 项目中,通常可以用来传递当前请求的上下文信息、用户Session、数据源切换等场景,一般是配合过滤器、AOP等技术实现自动设置、清除数据,以保证不会发生内存泄漏的情况。

为什么要把 ThreadLocalMap 的 key 设计成弱引用?

    本质原因还是为了解决 ThreadLocal 对象本身内存泄漏的问题。

    如果 key 设置为强引用,那么,当外部清除 ThreadLocal 对象的强引用时,此时 key 依然保持对 ThreadLocal 对象的强引用,导致 ThreadLocal 对象本身无法被回收。

public class Example {

    private ThreadLocal<String> local = new ThreadLocal<>();

    public void example() {
    
        local.set("存储数据");
    
        // 清除强引用
        local = null;
        
        // 如果 key 设置为强引用,当前线程的 ThreadLocalMap 还有强引用指向 ThreadLocal 对象(local)
        // 此时如果发生 GC 无法回收 local 对象占用的空间
        // System.gc();
    }
}