讲透ThreadLocal
一. 简介
ThreadLocal是JDK提供的一个工具类,其作用是在多线程共享资源的情况下,使每个线程持有一份该资源的副本,每个线程的副本都是独立互不影响的。线程操作各自的副本,这样就避免了资源竞争引发的线程安全问题。
二. 使用示例
模拟Spring中的事务管理,每个事务与当前线程绑定,不同线程的事务之间相互独立互不影响。代码如下:
public class TransactionManager {
//业务线程
private static final class BizTask implements Runnable{
private final ThreadLocal<Transaction> transaction;
public BizTask(){
//创建事务,并与当前线程绑定
transaction = ThreadLocal.withInitial(() -> {
long id = Thread.currentThread().getId();
return new Transaction(id);
});
}
@Override
public void run() {
transaction.get().begin();
System.err.println("执行业务逻辑");
try {
Thread.sleep(1000L);
transaction.get().commit();
transaction.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//事务
private static final class Transaction {
private long id;
private TransactionStatus status;
public Transaction(long id) {
this.id = id;
}
public void begin(){
status = TransactionStatus.BEGIN;
System.err.println("开启事务, id: " + id + ", status: " + status);
}
public void commit() {
status = TransactionStatus.COMMIT;
System.err.println("提交事务, id: " + id + ", status: " + status);
}
public void rollback() {
status = TransactionStatus.ROLLBACK;
System.err.println("回滚事务, id: " + id + ", status: " + status);
}
}
//事务状态
private enum TransactionStatus {
BEGIN,
COMMIT,
ROLLBACK,
}
public static void main(String[] args) throws InterruptedException {
int threadNum = 5;
ExecutorService executor = Executors.newFixedThreadPool(threadNum);
//执行业务逻辑,可以看到每个线程的事务相互独立,互不影响
for (int i = 0; i < threadNum; i++) {
executor.submit(new BizTask());
}
executor.shutdown();
}
}
结果如下:
开启事务, id: 17, status: BEGIN
执行业务逻辑
开启事务, id: 14, status: BEGIN
执行业务逻辑
开启事务, id: 13, status: BEGIN
执行业务逻辑
开启事务, id: 16, status: BEGIN
执行业务逻辑
开启事务, id: 15, status: BEGIN
执行业务逻辑
提交事务, id: 14, status: COMMIT
提交事务, id: 17, status: COMMIT
提交事务, id: 13, status: COMMIT
提交事务, id: 16, status: COMMIT
提交事务, id: 15, status: COMMIT
三. ThreadLocal源码分析
ThreadLocal,线程本地变量,该变量为每个线程私有。ThreadLocal类有一个内部类,名为ThreadLocalMap,可以理解为一个简化版的HashMap。源码如下:
static class ThreadLocalMap {
//该Map的Entry,Key为ThreadLocal实例,Value为ThreadLocal对象所引用的值。
//这里使用了WeakReference弱引用,当Entry为null时可以尽快被GC
static class Entry extends WeakReference<ThreadLocal<?>> {
//与ThreadLocal关联的对象引用,为当前Entry的value
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始容量16
private static final int INITIAL_CAPACITY = 16;
//使用数组保存了所有的线程本地变量
private Entry[] table;
}
其中Entry为ThreadLocalMap的一个内部类,与HashMap的Entry结构类似,都是key-value对的形式。它的Key为ThreadLocal实例,Value为ThreadLocal对象所引用的对象。ThreadLocalMap使用一个Entry[]数组保存了所有的线程本地变量,因为一个线程可以维护多个ThreadLocal实例。
ThreadLocalMap内部保存了众多的ThreadLocal对象,既然说ThreadLocal是线程私有的,那么ThreadLocalMap是存放在哪里呢?
Thread类有一个成员变量——threadLocals,它就是保存了与当前Thread关联的一个ThreadLocalMap,源码如下:
//当前线程内部维护的ThreadLocalMap对象,用于保存所有ThreadLocal实例
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到,ThreadLocalMap对象保存在了Thread的内部,也即当前线程的私有内存中。通过上面的分析,我们可以了解ThreadLocal变量的大致内存结构如下:
ThreadLocal的主要方法为get()、set()和initialValue()。首先看set():
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程关联的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//创建一个Entry,以当前ThreadLocal对象为Key,待存储对象为Value,保存在ThreadLocalMap中
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到,set()的逻辑很简单,从当前线程中获取ThreadLocalMap,然后将该ThreadLocal的值保存在里面。
再看get()方法:
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取当前线程关联的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//从ThreadLocalMap获取以ThreadLocal为key的Entry的value
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果当前ThreadLocalMap不存在,则调用setInitialValue()方法,获取初始值
return setInitialValue();
}
ThreadLocal还有一个方法initialValue(),该方法提供给子类覆盖,以在创建ThreadLocal时指定初始值。
四. 应用场景
ThreadLocal最常见的使用场景为管理数据库连接Connection对象等。Spring中使用ThreadLocal来设计TransactionSynchronizationManager类,实现了事务管理与数据访问服务的解耦,同时也保证了多线程环境下connection的线程安全问题。
五. ThreadLocal的内存泄漏问题
-
JVM引用类型
从源码中可以看出,ThreadLocalMap中Entry的key为ThreadLocal对象,并将其声明成了一个WeakReference弱引用。在分析其设计思想之前,先简单回顾下JVM中的几种引用类型:
- 强应用:普通的引用类型,被强引用的对象是一定不会被GC回收的。
- 软引用SoftReference:软引用一般用来实现内存敏感的缓存,如果有空闲内存就可以保留缓存,当内存不足时就清理掉,这样就保证使用缓存的同时不会耗尽内存。
- 弱引用WeakReference:它的生命周期比软引用还要短,在GC的时候,不管内存空间是否够用,都会回收WeakReference对象。
- 虚引用PhantomReference(较少使用):任何时候可能被GC回收,就像没有引用一样。
ThreadLocal之所以设计成WeakReference,目的就是当外部不再持有ThreadLocal的强引用时,尽快回收该ThreadLocalMap中对应的key。
-
ThreadLocal的内存泄漏问题
如前文所述,当ThreadLocal没有外部强引用来引用它时,ThreadLocal对象就会在下次JVM垃圾收集时被回收,这个时候就会出现Entry中Key已经被回收,但是Value仍然所在,即所谓的"null Key"情况。此时外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就会一直存在一条强引用链:Thread --> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收了,进而造成内存泄漏。
-
JDK对于ThreadLocal内存泄漏的解决方案
JDK团队已经考虑到这样的情况,并做了一些措施来使得ThreadLocal尽量不会出现内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候,会清除掉ThreadLocalMap的所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。
以ThreadLocal的get()方法为例:
public T get() { //获取当前线程实例 Thread t = Thread.currentThread(); //获取当前线程中的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { //通过ThreadLocalMap的getEntry()方法获取Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
它会调用ThreadLocalMap的getEntry()方法获取Entry实例:
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 //如果Entry获取不到,则调用getEntryAfterMiss() return getEntryAfterMiss(key, i, e); }
如果获取不到,说明该key不存在或已经被回收了,则进入getEntryAfterMiss()方法:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; //如果key为null,则清除该Entry项 if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
注意这里的k == null的情况,如果key为null,则执行expungeStaleEntry清除该Entry项。
此外,JDK推荐当ThreadLocal对象不再使用时,显式调用其remove()方法,清除该线程本地变量,最终也会调用上面的ThreadLocalMap.getEntryAfterMiss()方法。
综上,JDK也提供了相应的策略尽量避免ThreadLocal导致的内存泄漏问题,主要是在get()和remove()操作时清除已被回收的Entry项。但是该策略也并不是完美的,如果用户将ThreadLocal初始化后,再也不调用get()或remove()方法,则还是有内存泄漏的风险。
-
为什么要使用WeakReference?
从前文的描述中,很可能造成一种错觉:ThreadLocal由于使用了WeakReference而导致了内存泄漏。这其实是没有真正理解ThreadLocal内存泄漏的本质。首先我们来看看为什么要使用弱引用。下面是官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. Entry的key使用WeakReference弱引用,来处理大内存、长生命周期的线程的使用问题。
我们分别考虑下不使用WeakReferences和使用WeakReferences的情况下,分别会造成什么问题:
- 不使用WeakReferences,而使用强引用:外部引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致整个Entry内存泄漏。
- 使用使用WeakReferences:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set()、get()或remove()的时候会被清除。如果没有调用,则可能造成Entry的Value的内存泄漏。
由此可以看出,无论是否使用WeakReference,都有可能产生内存泄漏的情况,其根本原因在于ThreadLocalMap的生命周期与线程绑定。如果线程存活时间较长,且没有显式remove掉ThreadLocal对象,就有可能出现问题。而使用了WeakReference,至少可以保证无用的ThreadLocal对象被回收,不会出现整个Entry的内存泄漏,在一定程度上缓解了该问题。
-
总结
ThreadLocal的内存泄漏问题,根本原因是ThreadLocalMap的生命周期与Thread绑定,如果线程执行时间较长,则ThreadLocalMap就会一直不被GC回收。如果不显式调用remove()方法移除过期的ThreadLocal,则有可能造成内存泄漏。因此建议使用ThreadLocal时线程生命周期不要过长,且ThreadLocal对象使用完后显式调用remove()方法进行移除。