结构演进

早起JDK版本中,ThreadLocal内部结构是一个Map,线程为key,线程在“线程本地变量”中绑定的值为Value。每一个ThreadLocal实例拥有一个Map实例。(Key是线程,Value是值)

JDK8中,ThreadLocal内部结构发生了演进,虽然还是Map,但是拥有者变成了Thread实例,每一个Thread实例拥有一个Map实例。Map中的key变为ThreadLocal实例。(Key是ThreadLocal,Value是值)

新版ThreadLocalMap如图:

image

每一个线程在获取本地值时,都会将ThreadLocal实例作为Key从自己拥有的ThreadLocalMap中获取值,别的线程无法访问自己的ThreadLocalMap实例,达到相互隔离的目的。

ThreadLocal为什么会内存泄漏

我们知道,ThreadLocal是基于ThreadLocalMap实现的,这个Map的Entry继承了WeakReference,而Entry对象中的key使用了WeakReference封装,也就是说Entry中的key是一个弱引用类型,而弱引用类型只能存活在下次GC之前。如果一个线程调用ThreadLocal的set设置变量,当前ThreadLocalMap则新增一条记录,但发生一次垃圾回收,此时key值被回收,而value值依然存在内存中,由于当前线程一直存在,所以value值将一直被引用。这些被垃圾回收掉的key就存在一条引用链的关系一直存在:
Thread --> ThreadLocalMap -->Entry --> Value
这条引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用线程池的时候,这个线程执行任务结束,ThreadLocal对象被回收了,线程放回线程池中不销毁,这个线程一直不被使用,导致内存泄漏。

  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么这个期间就会发生内存泄漏。

我们只需要在使用完该key值之后,通过remove方法remove掉,就可以防止内存泄漏了。

Entry的Key使用弱引用

为什么不直接使用ThreadLocal实例作为Key呢?

key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get的时候会被清除。

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

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

弱引用:对象只能生存到下一次垃圾回收之前。

应用场景

线程隔离

这个的典型应用就是“数据库连接独享”。下面的代码来自Hibernate,代码通过ThreadLoacl进行数据库连接(Session)的线程本地化存储。

private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() {
    Session s = (Session) threadSession.get();
    if(s == null) {
        s = getSession();
        threadSession.set(s);
    }
    return s;
}

一个Session代表了一个数据库连接。通过以上代码可以看出,这个Session相当于线程的私有变量,不是所有线程共用的,其他线程是获取不到这个Session的。

一般来说,完成数据库操作之后程序会将Session关闭,节省资源。如果Session为共享的方式,如果某个线程将Session关闭,其他线程在操作Session时就会报错。所以通过ThreadLocal简单实现了数据库连接的安全使用。

Reference

《Java高并发核心编程》
https://www.cnblogs.com/arielmeng/p/15617405.html