ThreadLocal 用于在多线程环境中安全的保存线程本地变量,同一线程在某个地方保存数据,在随后的任意地方都可以获取到。线程之间 ThreadLocal 中的数据是互相隔离的,所以 ThreadLocal 变量是线程安全的,这可以避免某些情况下为了保证线程安全而采取同步操作带来的性能损失。

当创建一个 ThreadLocal 变量的时候,访问这个变量的每个线程都会有这个变量的本地副本。一般采用以下形式来创建一个 ThreadLocal 变量:

1
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

使用 threadLocal.set(21) 方法来保存数据,后续可以在同一线程的任意地方调用 threadLocal.get() 方法来获取当前线程所保存的副本的值,从而避免了线程安全的问题。

数据的安全保存

那么这个本地副本是怎么保存的呢?每个线程(Thread 类对象)中都会有一个 ThreadLocalMap 类型的变量 threadLocals 来保存 ThreadLocal 中的数据。

在保存数据的 threadLocal.set() 方法中,先通过 Thread.currentThread() 方法获取到当前线程,然后通过 getMap(Thread t) 方法获取到当前线程的 ThreadLocalMap 对象,再以当前 threadLocal 对象为 key、需要保存的数据对象为 value,构造一个键值对 Entry 对象保存在 ThreadLocalMap 中。之后调用 threadLocal.get() 方法取数据也是先获取到这个线程对象,然后以对应 threadLocal 对象为 key 从这个 ThreadLocalMap 中取出数据的。

注意,一个 ThreadLocal 只能存储一个 Object 对象,如果需要存储多个 Object 对象,那么就需要多个 ThreadLocal 来转化成多个 Entry 对象(键值对)进行存储。如下图所示:

可以看出,即使是同一个 threadLocal 变量,由于每个线程自己都有一个独立的 ThreadLocalMap,所以不同线程中保存的数据也是互不影响的。

内存泄露问题

通过上面的分析可以知道,当使用 ThreadLocal 保存一个 value 时,会构造一个键值对 Entry 对象插入到 ThreadLocalMap 的数组中。Entry 类的定义如下:

1
2
3
4
5
6
7
8
9
10
// java.lang.ThreadLocal.ThreadLocalMap.Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

可以看出,Entry 继承自弱引用 WeakReference 类。构建 Entry 对象时,key 是一个指向 ThreadLocal 对象的弱引用,value 则为强引用。

为什么要这么设计呢?如下图所示,对于一个 ThreadLocal 对象,通常会有两个引用指向它:一个是线程中声明的 threadLocal 变量,这是个强引用;另一个是线程底层 ThreadLocalMap 中键值对的 key,这是弱引用。

当我们不再需要使用某 ThreadLocal 对象时,会采用将变量设置为 null(threadLocal = null)的方式释放掉线程中 threadLocal 变量与对象之间的引用关系,方便 GC 对 ThreadLocal 对象的回收。但此时线程的 ThreadLocalMap 中还有 key 引用着这个 ThreadLocal 对象:如果这个引用是强引用,那么这个 ThreadLocal 对象就可能永远不会被回收了,造成内存泄露;但现在这里设计成弱引用,那么当垃圾收集器发现这个 ThreadLocal 对象只有弱引用相关联时,就会回收它的内存。

在当前 Entry 对象的 key 设计成弱引用的情况下,Entry 对象中的 value 仍存在着内存泄露的隐患。因为在垃圾回收 key 被清理掉的时候,强引用 value 中的对象并不会被清理掉。那么 ThreadLocalMap 中就会存在着 key 为 null 的 Entry 对象,其中的 value 无法访问到也无法被 GC 回收,从而造成内存泄露。

针对这种现象,ThreadLocal 内置的 get()、set() 会在特定的条件下对 ThreadLocalMap 中 key 为 null 的 Entry 对象进行清理。但为了避免仍然可能存在的内存泄露问题,作为最佳实践,应当在我们使用完 ThreadLocak 对象之后,主动调用 remove() 方法进行清理。

应用示例

线程安全的使用 SimpleDateFormat

SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量。如果定义为 static,必须加锁,或者使用 DateUtils 工具类。或者使用下面这种用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.text.SimpleDateFormat;

public class ThreadLocalExample implements Runnable{

// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm"));

@Override
public void run() {
System.out.println("Thread Name= " + Thread.currentThread().getName()
+ " default Formatter = " + formatter.get().toPattern());
//...
}
}

Netty 源码中的 FastThreadLocal