Skip to content

JUC ThreadLocal

本文介绍ThreadLocal相关知识。

1. 什么是ThreadLocal

ThreadLocal 是 Java 提供的一个类,用于创建线程局部变量(thread-local variable)。它的核心作用是为每个使用该变量的线程都提供一个独立的、变量的副本。

什么是线程局部变量? 简单来说,当你有一个 ThreadLocal 变量时:

  • 当线程 A 调用它的 get() 方法时,它会得到一个值。如果是第一次调用get()方法,则会返回初始值。
  • 当线程 B 调用它的 get() 方法时,它会得到另一个值(可能是同一个类的实例,但不是同一个对象)。
  • 线程 A 对这个值进行修改,不会影响线程 B 获取到的那个值。
  • 线程 B 对它自己的值进行修改,也不会影响线程 A 获取到的那个值。

ThreadLocal 提供了一种替代方案来管理“上下文”或“会话”级别的数据,特别适用于以下场景:

  • 避免同步的开销和复杂性: 如果某个数据在单个线程的整个执行过程中都需要用到,并且不需要被其他线程直接访问,那么与其使用共享变量加锁,不如使用 ThreadLocal 为每个线程维护一个独立副本。这样每个线程访问自己的副本时,就不需要加锁,避免了同步带来的性能损耗。

  • 方便地在方法调用链中存取数据: 有些数据(如用户身份信息、事务 ID、请求上下文等)需要在同一个线程内的多个方法中访问,但又不想作为参数层层传递。可以将这些数据存储在 ThreadLocal 中,方法只需要通过 ThreadLocal.get() 就能获取,而无需关心数据是从哪里来的。

    例如,在Spring Security中,用户登录信息就是存放在ThreadLocal中的,由于每个请求进来是一个线程处理的,所以过滤器(Filter)是在同一个线程进行处理的,把登录信息存在ThreadLocal中,可以使得后续的过滤器拿到用户信息,不用作为参数传递,降低代码复杂性。

2. 用法

ThreadLocal 的核心方法如下:

  1. set(T value): 设置当前线程中 ThreadLocal 变量的值为 value

  2. get(): 获取当前线程中 ThreadLocal 变量的值。

    如果当前线程是第一次调用 get(),并且之前没有调用过 set(),则会调用 initialValue() 方法获取初始值。

  3. initialValue(): 这是一个 protected 方法,通常通过继承 ThreadLocal 并覆盖此方法来提供一个默认值。当线程第一次调用 get() 且未设置值时,就会返回 initialValue() 的结果。

    Java 8 引入了更方便的 ThreadLocal.withInitial(Supplier) 方法来提供初始值。

  4. remove(): 移除当前线程中 ThreadLocal 变量的值。这非常重要,尤其是在线程池等复用线程的环境中,可以防止内存泄露问题。移除后,下次调用 get() 时会重新计算初始值。

下面以一个例子说明ThreadLocal的用法:

java
@Slf4j
public class ThreadLocalExample {

    // 创建一个 ThreadLocal 变量,使用 withInitial 提供默认值
    private static final ThreadLocal<String> threadLocalName = ThreadLocal.withInitial(() -> "Default Name");

    public static void main(String[] args) throws InterruptedException {

        // 线程 A
        Thread threadA = new Thread(() -> {
            log.info("Initial value: " + threadLocalName.get());

            threadLocalName.set("Thread A's Name");
            log.info("Set value: " + threadLocalName.get());

            Sleeper.sleep(1000);  // 睡眠1秒钟,期间其他线程改变ThreadLocal的值

            // 再次获取,验证值是线程隔离的
            log.info("Final value: " + threadLocalName.get());

            // !!!!! 重要:在线程结束前移除 ThreadLocal 变量的值 !!!!!
            threadLocalName.remove();
            log.info("After remove: " + threadLocalName.get()); // remove 后会再次回到 initialValue
        }, "Thread-A");


        // 线程 B
        Thread threadB = new Thread(() -> {
            log.info("Initial value: " + threadLocalName.get());

            threadLocalName.set("Thread B's Name");
            log.info("Set value: " + threadLocalName.get());

            Sleeper.sleep(2000);

        }, "Thread-B");

        threadA.start();
        Sleeper.sleep(200); // 让 A 先运行一会儿
        threadB.start();

        threadA.join(); // 等待线程 A 结束
        threadB.join(); // 等待线程 B 结束

        log.info("value: " + threadLocalName.get()); // 主线程也有自己的值
    }
}

结果如下:

txt
11:03:06.420 [Thread-A] INFO  : Initial value: Default Name
11:03:06.421 [Thread-A] INFO  : Set value: Thread A's Name
11:03:06.621 [Thread-B] INFO  : Initial value: Default Name
11:03:06.621 [Thread-B] INFO  : Set value: Thread B's Name
11:03:07.422 [Thread-A] INFO  : Final value: Thread A's Name
11:03:07.422 [Thread-A] INFO  : After remove: Default Name
11:03:08.626 [main] INFO  : value: Default Name

可以看到,ThreadLocal变量在每个线程中都有一份,互不干扰。

3. 原理

3.1 threadLocals字段

首先明确一个想法:在Java中,线程是由Thread类表示的,一个线程就是一个Thread类对象。

Thread类中有一个字段,用来存储ThreadLocal变量(这个变量是属于对象的,所以是线程私有的):

java
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap,观其名知其意,这是一个Map结构,所以存储的是key-value键值对,但是ThreadLocalMap有些许不同(ThreadLocalMapThreadLocal中的内部类,此处只显示ThreadLocalMap的结构):

java
static class ThreadLocalMap {
  // 存储键值对的数组
	private Entry[] table;
  
  // Entry是内部类
  static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

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

可以发现,ThreadLocalMap中的键值对,其中键是ThreadLocal变量,是一个弱引用,值就是我们实际存储的值(例如字符串、用户信息对象等等),是一个强引用。

当我们调用ThreadLocal.get()方法时:

java
public T get() {
  	// 获取当前线程
    Thread t = Thread.currentThread();
  	// 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      	// this 是ThreadLocal对象,即通过键获取键值对
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
          	// 返回值
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
  	// 如果map为空,则表示该线程中的map还没有创建,此时应该创建map,返回初始值
    return setInitialValue();
}

setInitialValue()方法如下:

java
private T setInitialValue() {
  	// 获取初始值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      	// 在map中存入键值对
        map.set(this, value);
    } else {
      	// 创建线程对应的map
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

// 创建map
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

至此,关于如何实现线程私有变量就很清晰了,就是在Thread中保存一份Map,其中存储了以ThreadLocal变量为键,以应用实际值为值的键值对。

3.2 inheritableThreadLocals

观察Thread的源码,发现不仅有threadLocals字段,还有inheritableThreadLocals字段:

java
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

这又是什么呢?

在某些场景下,父线程创建子线程时,需要将父线程的某些上下文信息传递给子线程,让子线程能够访问到这些信息。这种需求被称为上下文传播(Context Propagation)。我们就可以使用InheritableThreadLocal来实现。

当一个线程(父线程)创建另一个新线程(子线程)时,子线程会继承父线程中所有 InheritableThreadLocal 变量的当前值。 子线程会在自己的 InheritableThreadLocalMap 中拥有这些值的副本。

使用场景:

  • 用户身份信息传递:在处理用户请求时,如果主线程(通常是线程池中的某个线程)验证了用户身份,将用户信息存入 ThreadLocal。如果这个请求又需要创建子线程去执行一些并行任务(如异步发送邮件、记录日志、调用其他内部服务),这些子线程可能需要知道当前是哪个用户在操作,以便进行权限检查、日志记录等。
  • 请求 ID / 跟踪 ID: 在分布式系统中,一个请求会跨越多个服务和线程。通常会生成一个唯一的请求 ID 或跟踪 ID 并将其贯穿整个调用链,方便日志分析和问题追踪。将这个 ID 存在 ThreadLocal 中,子线程需要继承这个 ID。

inheritableThreadLocals的工作原理:

在Thread的构造方法内,有以下代码(只展示关键代码)体现线程间的继承:

java
private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
  	// inheritThreadLocals默认为true
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

ThreadLocal.createInheritedMap代码如下:

java
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
		// 将父线程中的inheriyableMap复制一份到子线程
    for (Entry e : parentTable) {
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

inheritableThreadLocals字段是在什么时候初始化的呢?InheritableThreadLocalThreadLocal的一个子类:

java
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    public InheritableThreadLocal() {}

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

当我们调用ThreadLocal.get()方法时,如果是第一次使用ThreadLocal变量,那么最终会调用createMap()方法,这就在线程中初始化了inheritableThreadLocals字段。

不仅仅是get()方法会创建ThreadLocalMapset()方法也会:

java
// ThreadLocal类中的set()方法
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

下面举一个例子说明InheritableThreadLocal的使用:

java
@Slf4j
public class InheritableThreadLocalExample {

    // 创建一个 InheritableThreadLocal 变量, 子线程会继承父线程设置的这个变量的值
    private static final InheritableThreadLocal<NumberHolder> inheritableContext1 = new InheritableThreadLocal<>();
    private static final InheritableThreadLocal<NumberHolder> inheritableContext2 = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {

        inheritableContext1.set(new NumberHolder(1));
        inheritableContext2.set(new NumberHolder(1));

        Sleeper.sleep(100);  // 睡眠一会儿,确保子线程后执行

        // 创建并启动子线程
        Thread childThread = new Thread(() -> {
            // 在子线程中获取 InheritableThreadLocal 的值,应该能获取到父线程在创建子线程前设置的值
            NumberHolder numberHolder1 = inheritableContext1.get();
            log.info("子线程第一次获取(inheritableContext1):" + numberHolder1.toString());
            NumberHolder numberHolder2 = inheritableContext2.get();
            log.info("子线程第一次获取(inheritableContext2):" + numberHolder2.toString());

            // (可选) 在子线程中修改 InheritableThreadLocal 的值,注意,没有修改引用地址
            numberHolder1.setNum(2);
            log.info("修改后 inheritableContext1 值: " + inheritableContext1.get());
            inheritableContext2.set(new NumberHolder(3));
            log.info("修改后 inheritableContext2 值: " + inheritableContext2.get());

        }, "Child-Thread");
        childThread.start();

        // 等待子线程执行完毕,以确保看到子线程的输出
        childThread.join();

        // 在父线程中再次获取值,子线程修改结果影响到了父线程
        log.info("子线程结束后,inheritableContext1 值: " + inheritableContext1.get());
        // 应该还是父线程设置的原始值
        log.info("子线程结束后,inheritableContext2 值: " + inheritableContext2.get());

    }
}

@Data
@AllArgsConstructor
class NumberHolder{
    private int num;
}

结果如下:

txt
12:04:00.100 [Child-Thread] INFO  : 子线程第一次获取(inheritableContext1):NumberHolder(num=1)
12:04:00.101 [Child-Thread] INFO  : 子线程第一次获取(inheritableContext2):NumberHolder(num=1)
12:04:00.101 [Child-Thread] INFO  : 修改后 inheritableContext1 值: NumberHolder(num=2)
12:04:00.101 [Child-Thread] INFO  : 修改后 inheritableContext2 值: NumberHolder(num=3)
12:04:00.101 [main] INFO  : 子线程结束后,inheritableContext1 值: NumberHolder(num=2)
12:04:00.101 [main] INFO  : 子线程结束后,inheritableContext2 值: NumberHolder(num=1)

可以发现,父子线程中ThreadLocal引用的值,其实是指向同一块内存空间的,如果子线程修改了对象内容而不改变引用地址,那么会影响父线程中ThreadLocal引用的值;如果改变了引用地址,那么不会影响父线程。

4. 内存泄漏问题

4.1 为什么要使用弱引用作为键的类型

假设我们使用强引用作为键的类型,那么根据之前的原理分析,可以画出下面的对象关系图:

image-20250515140322029

当有其他指针指向ThreadLocal对象时,此时说明我们的应用程序还需要ThreadLocal对象,它不应该被当成垃圾回收掉。

但是,如果之后我们应用程序不需要ThreadLocal对象后,对象关系图如下:

image-20250515140920654

由于在应用程序中,没有指针指向ThreadLocal对象,那么该对象应该被当成垃圾回收掉。但是,从Thread这一条线看过来,会发现还有强引用指向ThreadLocal对象,此时该对象不会被当成垃圾回收掉,但是,我们程序中再也不能通过ThreadLocal变量使用ThreadLocal对象了,此时会造成内存泄漏

因此,使用弱引用,当没有强引用指向ThreadLocal对象时,该对象会被当成垃圾回收掉:

image-20250515141423522

4.2 Entry中value造成的内存泄漏问题

在Java中,键被设计为弱引用解决内存泄漏问题是一方面,但是这又带来了值造成内存泄漏的问题。

当作为key的ThreadLocal对象被当成垃圾回收后,key变为null,但是此时value还强引用地指向数据对象:

image-20250515142235826

Thread对象出发,可以到达value指向的数据对象,所以该数据对象不会被当成垃圾释放掉,但是,实际上该对象已不可达,实际上是垃圾,所以这就是另一种内存泄漏。

要解决这个问题,在线程中使用完ThreadLocal后,需要开发者手动调用remove()方法,用来移除Entry

java
// ThreadLocal中的remove()
public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null) {
         m.remove(this);
     }
 }

// ThreadLocalMap中的remove()
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
  	// 通过key找到第一个索引值
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i]; e != null; 
         // 继续找下一个索引值
         e = tab[i = nextIndex(i, len)]) {
      	// 如果该索引值处的key等于要移除的key,那么执行移除操作
        if (e.refersTo(key)) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

所以,使用ThreadLocal的“正确方式”应如下:

java
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"default");
new Thread(()->{
    try {
        // 使用
      	threadLocal.get()
    }finally {
        // 移除
        threadLocal.remove();
    }
},"t1").start();

4.3 final 关键字的使用

在上面所谓的“正确方式”,其实还有一个问题,来看下面的例子:

java
private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"default");

public static void main(String[] args) {
    new Thread(()->{
        try {
            // 使用
            log.info(threadLocal.get());

            Sleeper.sleep(2000);

            log.info(threadLocal.get());
            System.gc();

            // 下面是为了调试所需
            Thread thread = Thread.currentThread();
            log.info(thread.getName());

        }finally {
            // 移除
            threadLocal.remove();
        }
    },"t1").start();

    Sleeper.sleep(100);
    // 改变threadLocal指向的对象
    threadLocal = ThreadLocal.withInitial(()->"!!!!!!!");

}

运行上面的程序,结果如下:

java
14:52:53.136 [t1] INFO  : default
14:52:55.142 [t1] INFO  : !!!!!!!
14:52:55.152 [t1] INFO  : t1

这是正确的,但是上面的程序隐含着内存泄漏问题,我们在第16行代码打上断点,检查t1线程的threadLocals字段:

image-20250515145529608

我们分析上面的代码流程:

  • 首先ThreadLocal变量threadLocal指向第一个对象(我们称为A);
  • 然后启动t1线程,使用threadLocal变量,此时在其threadLocalMap中添加一条键值对(A-default字符串);
  • t1线程休眠2秒,模拟执行其他任务;
  • 主线程在t1线程执行后开始执行,将ThreadLocal变量threadLocal指向第二个对象(我们称为B);
  • t1线程结束休眠后,再次使用threadLocal变量,此时由于threadLocal所指向的对象已经变了 ,所以会在t1线程的threadLocalMap中再添加一条键值对(B-!!!!!!!字符串);
  • t1线程执行垃圾回收,由于第一个ThreadLocal对象A没有强引用指向它,所以被回收掉,第一条键值对中的键(referent字段)变为null;如果线程t1长时间存活,那么第一条键值对中的值(default字符串)会造成内存泄漏。
  • 即使t1线程最后使用完ThreadLocal调用了remove()方法,也只是释放第二条键值对,第一条键值对仍然保留。

出现上面的问题,就是由于ThreadLocal变量threadLocal在线程使用过程中发生了改变,所以我们应该限制ThreadLocal变量threadLocal不允许改变,加final关键字。

最终,总结出使用ThreadLocal的正确用法:

java
// final关键字保持threadLocal指向的ThreadLocal对象不变
final ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"default");
new Thread(()->{
    try {
        // 使用
      	threadLocal.get()
    }finally {
        // 移除
        threadLocal.remove();
    }
},"t1").start();