ThreadLocal
2025-01-22 08:19:30    987 字   
This post is also available in English and alternative languages.

ThreadLocal 并不是 Thread,而是 Thread 局部变量。
它不是为了解决多线程访问共享变量,而是为了数据隔离;它为每个线程创建一个单独的变量副本,保证各个线程里的变量独立于其他线程内的变量。


1. ThreadLocal 流程简单梳理

当调用 ThreadLocalset方法写入数据时,数据最终都是存储在调用set方法时所在的线程绑定的Thread实例;
java.lang.Thread类中有一个 threadLocals 字段,用于存储 ThreadLocal 写入的数据(第一次看 ThreadLocal 源码时,容易被绕进去的地方, ThreadLocal 本身并不存储数据);

1
2
3
4
5
public class Thread implements Runnable {
// ...
ThreadLocal.ThreadLocalMap threadLocals = null;
// ...
}

java.lang.Thread类的 threadLocals 字段初始化是在 ThreadLocal 中完成的,当调用 ThreadLocalsetget方法写入数据时,如果当前线程的Thread实例的 threadLocals 字段为空,会进行初始化。

1
2
3
4
5
// java.lang.ThreadLocal#createMap

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

ThreadLocalMap 的构造方法要求传入一个Key-Value,而 ThreadLocal 在创建 ThreadLocalMap 时,传入的Key是this,说明 ThreadLocalMap 的key类型是 ThreadLocal

ThreadLocalMap 并非Java.util包下的HashMap,而是 ThreadLocal 定义的一个静态内部类。与HashMap的实现不同, ThreadLocalMap 使用开放定址法解决Hash冲突,是一个简单的Map实现。
与其说 ThreadLocalMap 是一个Map,不如说是一个数组;组数元素的类型为ThreadLocalMap.Entry,在插入元素时,会根据元素持有的弱引用对象计算出要插入的数组下标。

实际的项目中,一个请求通常是在一个线程中完成的,在其整个链路处理中,可能会存在多个 ThreadLocal
例如:Spring事务处理的 ThreadLocal 、Dubbo调用(RpcContext)的 ThreadLocal ,即一个请求即用到Dubbo发起RPC调用,又用到事务注解操作数据库;
那么至少就存在两个 ThreadLocal 往一个线程Thread实例的 threadLocals 字段写数据。

ThreadLocalMap 的key作用就是区分同一个线程Thread对象中不同 ThreadLocal 写入的数据,实现数据隔离。


2. ThreadLocal 为什么会内存泄漏?

ThreadLocalMap 使用 ThreadLocal 的弱引用作为key;

如果一个 ThreadLocal 没有外部强引用引用它,那么GC时,这个 ThreadLocal 势必会被回收,这样一来, ThreadLocalMap 中就会出现key为null的Entry;

key为null的Entry永远不会被访问,如果当前线程迟迟不结束(比如正好用在线程池),这些key为null的Entry的value就会一直存在;

在不使用某个 ThreadLocal 对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的 ThreadLocalMap 对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。


3. ThreadLocal测试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j
@Component
public class ThreadLocalMainApp02 {

private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

public static void main(String[] args) throws InterruptedException {
ThreadLocalMainApp02 mainApp = new ThreadLocalMainApp02();
mainApp.process();
}

private void process() throws InterruptedException {
//主线程设值并打印
threadLocal.set(1);

//子线程设值并打印
new Thread(() -> {
threadLocal.set("9527");
log.info("#999 当前线程名称:[{}],内部数据:[{}]", Thread.currentThread().getName(), threadLocal.get());
}).start();

log.info("#1 当前线程名称:[{}],内部数据:[{}]", Thread.currentThread().getName(), threadLocal.get());

// 确保执行完成
TimeUnit.SECONDS.sleep(2);
}
}

输出结果:

1
2
14:02:23.988 [Thread-0] INFO com.xxx.ThreadLocalMainApp02 - #999 当前线程名称:[Thread-0],内部数据:[9527]
14:02:23.988 [main] INFO com.xxx.ThreadLocalMainApp02 - #1 当前线程名称:[main],内部数据:[1]

观察输出结果,main线程 和 Thread-0 两个线程输出了不同的值,但它们操作的实例对象却是同一个。


4. Reference