一. ThreadLocal简介
在日常的 Java 开发中,我们经常会遇到这样一类问题:如何在不增加方法参数传递成本的情况下,在同一个线程的多个方法调用之间共享数据?例如,请求链路中的用户信息、数据库连接、事务上下文等,这些数据本质上是"与线程绑定"的,但又不适合通过全局共享变量来管理,因为全局变量在多线程环境下会引发并发安全问题。这个时候,ThreadLocal 就成为了一种非常优雅且高效的解决方案。
ThreadLocal 的核心作用可以用一句话概括:为每个线程提供一份独立的变量副本,实现线程之间的数据隔离 。与我们平时使用的共享变量不同,ThreadLocal 并不是让多个线程去竞争同一份数据,而是让每个线程都拥有属于自己的"私有变量",从而避免了加锁、竞争等并发问题。这种设计本质上是一种"用空间换时间"的思想,通过牺牲一定的内存来换取更高的执行效率和更简单的编程模型。
在实际业务开发中,ThreadLocal 的使用场景非常广泛。例如,在 Web 请求处理中,我们可以通过 ThreadLocal 保存当前登录用户的信息,这样在整个请求处理链路中,无需层层传递参数就可以随时获取用户上下文;在数据库访问中,可以将 Connection 绑定到当前线程,实现同一线程内的事务一致性;在一些框架(如 Spring、MyBatis)中,ThreadLocal 更是被大量用于管理线程级别的资源和上下文信息。可以说,ThreadLocal 是构建"线程上下文(Thread Context)"的核心工具之一。
二. ThreadLocal使用示例
ThreadLocal的使用简单示例:
java
```java
/**
* 线程上下文(ThreadLocal 实现)
*
* 用于在同一个线程内共享请求级数据,例如用户信息、请求ID等
*/
public final class ThreadContext {
/**
* 每个线程独享一份 RequestContext
*/
private static final ThreadLocal<RequestContext> CONTEXT =
ThreadLocal.withInitial(RequestContext::new);
private ThreadContext() {
}
/**
* 获取当前线程的上下文对象
*/
public static RequestContext getContext() {
return CONTEXT.get();
}
/**
* 设置用户信息
*/
public static void setUserInfo(UserInfo userInfo) {
CONTEXT.get().setUserInfo(userInfo);
}
/**
* 获取用户信息
*/
public static UserInfo getUserInfo() {
return CONTEXT.get().getUserInfo();
}
/**
* 设置请求ID
*/
public static void setRequestId(String requestId) {
CONTEXT.get().setRequestId(requestId);
}
/**
* 获取请求ID
*/
public static String getRequestId() {
return CONTEXT.get().getRequestId();
}
/**
* 清理 ThreadLocal(必须在请求结束时调用)
*/
public static void remove() {
CONTEXT.remove();
}
}
/**
* 请求上下文对象
*
* 存储与当前请求相关的数据(线程隔离)
*/
public class RequestContext {
/**
* 用户信息
*/
private UserInfo userInfo;
/**
* 请求ID
*/
private String requestId;
// ===== getter / setter =====
public UserInfo getUserInfo() {
return userInfo;
}
public void setUserInfo(UserInfo userInfo) {
this.userInfo = userInfo;
}
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
}
三. 源码解析
1. get() 的核心源码
java
/**
* 获取当前线程中与这个 ThreadLocal 关联的值
*/
public T get() {
// 1. 先拿到当前线程对象
Thread t = Thread.currentThread();
// 2. 再从当前线程中取出 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 3. 如果当前线程已经有 ThreadLocalMap,则尝试根据 this 作为 key 查找
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 4. 如果当前线程还没有 map,或者 map 中没有当前 key,
// 就进入初始化流程
return setInitialValue();
}
/**
* 获取某个线程内部持有的 ThreadLocalMap
*
* 注意:
* ThreadLocalMap 是挂在线程对象 Thread 上的,
* 不是挂在 ThreadLocal 自己身上的。
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 当前线程第一次访问该 ThreadLocal 时,会走这个逻辑
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
return value;
}
/**
* 当线程第一次使用 ThreadLocal,并且 threadLocals 还不存在时,
* 创建一个新的 ThreadLocalMap
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
2. set() 的核心源码
java
/**
* 给当前线程设置一个与该 ThreadLocal 关联的值
*/
public void set(T value) {
// 1. 获取当前线程
Thread t = Thread.currentThread();
// 2. 获取当前线程内部的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 3. 如果 map 已存在,则直接放入
map.set(this, value);
} else {
// 4. 如果 map 不存在,则为当前线程创建一个新的 map
createMap(t, value);
}
}
也就是说,ThreadLocal 并没有"自己保存一份值",而是把值交给当前线程去保存。
因此,不同线程调用同一个 ThreadLocal.set(),最终写入的是各自线程内部的独立数据,互不影响。
java
public class Thread implements Runnable {
/**
* 当前线程持有的 ThreadLocalMap
*
* 这个字段专门存放普通 ThreadLocal 的数据。
* 每个线程对象都有自己独立的一份。
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/**
* 这个字段用于 InheritableThreadLocal
*
* 父线程创建子线程时,可以把值传递给子线程。
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
很多人刚接触 ThreadLocal 时会误以为:ThreadLocal 内部维护了一个 Map,key 是线程,value 是数据。
其实正好相反。真实结构是:每个线程内部维护一个 ThreadLocalMap,key 是 ThreadLocal,value 是线程私有数据 。这也是为什么 ThreadLocal 能做到线程隔离的根本原因。
3. ThreadLocalMap 的 Entry 结构
java
static class ThreadLocalMap {
/**
* Entry 继承 WeakReference
*
* 也就是说:
* key(ThreadLocal)是弱引用
* value(真正保存的数据)是强引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/**
* 与 ThreadLocal 关联的实际值
*/
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 弱引用保存 key
value = v; // 强引用保存 value
}
}
}
ThreadLocalMap.Entry 并不是普通的键值对,而是做了一个特殊设计:
key:WeakReference<ThreadLocal<?>>value:普通强引用对象
也就是说,key 是弱引用,value 是强引用。
这样设计的目的,是为了避免某个 ThreadLocal 对象已经不用了,却因为还被 ThreadLocalMap 强引用着而无法回收。
但这也带来了一个副作用:如果 ThreadLocal 被回收了,而线程还活着,那么 Entry 就可能变成:
java
key = null
value = 业务对象
这就是所谓的"脏 Entry",如果不及时清理,就可能造成内存长期占用。
4. ThreadLocalMap 的底层数组结构
java
static class ThreadLocalMap {
/**
* 初始容量,必须是 2 的幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 真正存储数据的数组
*
* 每个元素都是一个 Entry:
* key = ThreadLocal(弱引用)
* value = 线程私有数据
*/
private Entry[] table;
/**
* 当前数组中已使用的 Entry 数量
*/
private int size = 0;
/**
* 扩容阈值
*/
private int threshold;
/**
* 创建 ThreadLocalMap,并把第一条数据直接放进去
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
// 计算数组下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 存入第一条 Entry
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置扩容阈值
threshold = setThreshold(INITIAL_CAPACITY);
}
}
ThreadLocalMap 的底层不是 HashMap,而是一个定制化的数组结构。它使用数组来保存 Entry,并通过 threadLocalHashCode & (len - 1) 的方式定位下标。由于数组长度总是 2 的幂,因此这种位运算定位效率很高。同时,ThreadLocalMap 没有采用链表法解决冲突,而是采用了开放地址法,也就是冲突后继续往后找空位。
这样做的好处是:
- 结构更紧凑
- 减少链表节点带来的额外对象和指针跳转
- 更有利于 CPU cache 命中
- 对
ThreadLocal这种典型小规模存储场景更加高效
5. ThreadLocalMap 的 getEntry():如何查找元素
java
static class ThreadLocalMap {
/**
* 根据 ThreadLocal key 获取对应 Entry
*/
private Entry getEntry(ThreadLocal<?> key) {
// 先根据 hash 直接定位一个下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果正好命中,直接返回
if (e != null && e.get() == key) {
return e;
}
// 如果没命中,进入线性探测
return getEntryAfterMiss(key, i, e);
}
/**
* 发生哈希冲突后,继续向后查找
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到了对应的 key
if (k == key) {
return e;
}
// 如果发现 key 已经被 GC 回收了,则顺便清理脏数据
if (k == null) {
expungeStaleEntry(i);
} else {
// 继续向后找,下标到头后回环到 0
i = nextIndex(i, len);
}
e = tab[i];
}
return null;
}
/**
* 线性探测:下一个下标
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
}
ThreadLocalMap 查找元素时,先根据 hash 值定位数组下标。如果当前位置不是目标 key,就说明发生了冲突,这时不会像 HashMap 那样走链表,而是采用线性探测:
- 从当前位置往后逐个查找
- 数组到尾后回到头部继续找
- 直到找到目标 key 或遇到空槽位为止
同时,如果在线性探测过程中发现某个 Entry 的 key 已经变成 null,说明这个 ThreadLocal 已被 GC 回收,此时还会顺带触发脏数据清理。
ThreadLocalMap 没有使用 HashMap 的链表或红黑树结构,而是采用了数组加开放地址法(线性探测)。这种设计不会导致查找错误,因为插入和查找都遵循同样的探测路径:从 hash 位置开始,逐步向后查找,直到找到目标 key 或空槽位。虽然理论上可能退化为线性扫描,但 ThreadLocalMap 的数据量通常很小,加上使用黄金分割 hash 使分布均匀,因此冲突概率很低。
相比 HashMap,这种结构的优势在于:
- 使用连续内存,CPU cache 友好
- 没有链表指针,减少内存访问跳跃
- 对象数量更少,降低 GC 压力
因此它更适合 ThreadLocal 这种"小数据、高频访问"的场景,而不是通用的大规模数据存储。
6. ThreadLocalMap 的 set():如何插入元素
java
static class ThreadLocalMap {
/**
* 向当前线程的 ThreadLocalMap 中设置值
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 先根据 key 的 hash 定位下标
int i = key.threadLocalHashCode & (len - 1);
// 使用开放地址法处理冲突
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果 key 已存在,则直接覆盖 value
if (k == key) {
e.value = value;
return;
}
// 如果发现某个脏 Entry(key 已被 GC 回收),
// 则进入替换脏槽位的逻辑
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找到空槽位,直接插入
tab[i] = new Entry(key, value);
int sz = ++size;
// 插入后尝试清理脏 Entry;如果没清理掉太多且容量达到阈值,则扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold) {
rehash();
}
}
}
插入过程可以概括为三种情况:
- 第一种,如果当前 key 已经存在,那么直接覆盖旧值。
- 第二种,如果探测过程中发现某个槽位的 key 已经是 null,说明这里存在脏 Entry,此时不会简单跳过,而是触发专门的替换和清理逻辑。
- 第三种,如果一路探测后找到了空槽位,就直接插入新的 Entry。
这说明 ThreadLocalMap 在写入时,不仅做插入,还会顺带做一部分垃圾清理工作。这也是它"懒清理"设计的一部分。
四. 为什么ThreadLocalMap的key为什么要使用弱引用
ThreadLocalMap 的 key 使用弱引用,是为了避免 ThreadLocal 对象失去外部引用后,仍然被 Thread 持有,导致无法被 GC 回收,从而产生内存泄漏。
java
ThreadLocal
│
│(作为 key)
│
│
│
Thread(线程) ↓
└── ThreadLocalMap
└── Entry[]
├── key = ThreadLocal(弱引用)
└── value = 业务数据(强引用)
上面是ThreadLocal的结构关系图,从线程角度看应该是:
java
ThreadLocal(入口)
│
├───────────────────────────┐
│ │
↓ ↓
线程A 线程B
│ │
↓ ↓
ThreadLocalMap ThreadLocalMap
│ │
├── Entry ├── Entry
│ key = ThreadLocal │ key = ThreadLocal
│ value = A │ value = B
从上面的结构中可以看出:
我们常用的ThreadLocal可以是:static final的,也可以是我们随意创建的,但是:
Thread强引用ThreadLocalMapThreadLocalMap强引用EntryEntry:
key= 弱引用ThreadLocal
value= 强引用对象
如果ThreadLocal的key是强引用,当有如下代码:
java
ThreadLocal tl = new ThreadLocal();
tl.set(new BigObject());
// 这里把引用断掉
tl = null;
这个时候GC回收不了创建的这个ThreadLocal tl,原因就是引用关系是:Thread → ThreadLocalMap → Entry → key(ThreadLocal)。
所以Entry的key被设计成了弱引用,当是弱引用时,GC回收时ThreadLocal就可以被回收了,Entry.key → 变成 null,同时结构变成:Entry(null, value),所以就会有新的问题:value 还在 。因为value是强引用关系是:Thread → ThreadLocalMap → Entry → value。
所以我们在使用ThreadLocal的时候一定要注意及时清理,如果不调用 remove,仍然可能出现 value 无法释放的问题。因此 ThreadLocal 采用的是"弱引用 + 懒清理"的策略,并要求开发者在使用完后手动调用 remove,特别是在使用线程池时(线程复用)。
那么为啥不把value设置成弱引用呢?因为弱引用在一次GC之后会被回收,所以value不能是弱引用,那么这个时候你会不会想到:那为啥key作为弱引用就不GC回收呢?
原因是我们使用的时候,创建的ThreadLocal都是在代码中有引用的,或者像文章开头的例子中一样,使用static 修饰,这个时候我们创建的ThreadLocal引用是一直存在的,所以ThreadLocal作为弱引用的key不会担心被回收。
五. 总结
ThreadLocal 的设计本质上并不是"一个全局容器按线程存值",而是"每个线程内部维护一个私有 Map"。
其中,ThreadLocal 自身只负责提供访问入口和 key,真正的数据存储在线程对象的 ThreadLocalMap 中。
ThreadLocalMap 又不是普通的 HashMap,而是一个基于数组和开放地址法实现的定制结构,它通过特殊的 hash 递增策略降低冲突,并在 get、set、remove 过程中顺带清理脏 Entry。
同时,由于 Entry 的 key 是弱引用、value 是强引用,因此如果不及时 remove(),在线程长期存活的场景下就可能留下 value 无法及时释放的问题,这也是 ThreadLocal 使用中最经典的风险点。