引用结构

每个线程都有一个ThreadLocal.ThreadLocalMap类型的threadLocals属性, ThreadLocalMap中使用Entry来存储线程相关变量,Entrykey是对ThreadLocal的一个弱引用,value则是Object类型的线程变量。如下图: threadlocal

内存泄漏

从引用结构上可以看到,若ThreadLocal没有被GC Roots强引用时,在垃圾回收后,ThreadLocal则会被回收, Entry的key无引用。若线程一直不退出,则value就一直不会被回收,造成内存泄漏。如下图: threadlocal2

泄露演示

public class ThreadLocalCode {

    public static void main(String[] args) {
        // 第一步:模拟ThreadLocal执行后,丢失GCRoots的强引用
        setThreadLocal();
        // 第二步:手动gc(断点位置1:gc前查看Thread对象)
        System.gc();
        // 第三步: gc后再次查看Thread对象,(断点位置2)
        System.out.println();
    }

    public static void setThreadLocal() {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("123");
    }
}
断点1位置的Thread#ThreadLocalMap状态

alt 由图可见,此时Entry的key(即referent)和value均存在。

断点2位置的Thread#ThreadLocalMap状态

alt 由图可见,此时的key被回收了,只要当前线程不结束,这个Entry会因为没有key而找不到,所以会一直回收不了,由此可知,像setThreadLocal这种方式在项目中多次使用的结果。

正确使用

相信看了上面的分析,大家应该都知道怎么使用ThreadLocal了。如果项目中使用的Thread不会终止,我们只需要保证ThreadLocal变量不要被回收掉即可,我们知道ThreadLocal变量一是被Entry的key引用,但这是一个弱引用,所以我们可以再加一个强引用,比如将ThreadLocal变量定义为静态变量。同时我们也要保持一个良好的编码习惯,在使用完ThreadLocal变量后手动remove一下,做到万无一失。