目录
ThreadLocal是Java中一个非常重要且常用的线程局部变量工具类,它使得每个线程可以独立地持有自己的变量副本,而不是共享变量,解决了多线程环境下变量共享的线程安全问题。下面我将从多个维度深入分析ThreadLocal的内部结构和工作原理。
一、ThreadLocal
// 1. 初始化:创建ThreadLocal变量
private static ThreadLocal<T> threadLocal = new ThreadLocal<>();
// 2. 设置值:为当前线程设置值
threadLocal.set(value); // value为要存储的泛型对象
// 3. 获取值:获取当前线程的值
T value = threadLocal.get(); // 返回当前线程存储的值
// 4. 移除值:清除当前线程的ThreadLocal变量(防止内存泄漏)
threadLocal.remove();
【注】使用时,通常将ThreadLocal声明为
static final以保证全局唯一性private static ThreadLocal<T> threadLocal = ThreadLocal.withInitial(() -> initialValue);【注:】 withInitial里面放的是任何能够返回 T 类型实例的 Lambda / Supplier。
只要 Supplier 的逻辑最终能 new(或从缓存、工厂、单例池等)拿出一个 T,就合法。
例子
例子中看似我是在讲add()方法,但是要知道ThreadLocal里面底层是没有add这个方法的,这只是实现的一个业务逻辑,而底层是用get()方法实现的,所以下面的源码解析,也是在解析get()方法。
package com.qcby.test;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocalTest {
private List<String> messages = new ArrayList<>();
public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);
public static void add(String message) {
holder.get().messages.add(message);
}
public static List<String> clear() {
List<String> messages = holder.get().messages;
holder.remove();
return messages;
}
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
// 提交10个任务
for (int i = 0; i < 10; i++) {
final int threadId = i;
executor.submit(() -> {
ThreadLocalTest.add("线程" + threadId + "的消息" );
// 打印当前线程的消息
System.out.println("线程" + threadId + "的消息列表: " + holder.get().messages);
// 清除当前线程的ThreadLocal
ThreadLocalTest.clear();
});
}
// 关闭线程池
executor.shutdown();
executor.awaitTermination(1, TimeUnit.SECONDS);
// 主线程检查自己的ThreadLocal(应该是空的)
System.out.println("主线程的消息列表: " + holder.get().messages);
}
}
内部结构分析
根据这里get的源码追溯分析:

追溯到:

ThreadLocal.get()源码解析
/**
* 获取当前线程的ThreadLocal变量值
*/
public T get() {
// 1. 获取当前线程对象
Thread t = Thread.currentThread();
// 2. 获取当前线程的ThreadLocalMap(线程私有数据存储结构)
ThreadLocalMap map = getMap(t);
// 3. 如果map已存在
if (map != null) {
// 3.1 以当前ThreadLocal实例为key(也就是代码中的holder),获取对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 3.2 如果Entry存在
if (e != null) {
// 3.2.1 强转为泛型类型并返回值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 4. 如果map不存在或未找到值,初始化并返回默认值
return setInitialValue();
}
/**
* 获取线程的ThreadLocalMap(实际是Thread类的threadLocals字段)
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 直接返回线程对象的成员变量
}
/**
* 初始化值并存入ThreadLocalMap
*/
private T setInitialValue() {
// 1. 获取初始值(子类可重写initialValue()方法)
T value = initialValue();
// 2. 获取当前线程
Thread t = Thread.currentThread();
// 3. 获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 4. 如果map已存在,直接设置值
if (map != null) {
map.set(this, value);
} else {
// 5. 如果map不存在,创建新map并存入初始值
createMap(t, value);
}
// 6. 返回初始值
return value;
}
/**
* 创建线程的ThreadLocalMap并存入第一个值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 默认初始值实现(可被withInitial覆盖)
*/
protected T initialValue() {
return null; // 默认返回null
}
图示详解

所以执行结果:

可以看见一个线程中只有一个信息,而不是它们统一堆砌在一起,原因就是底层是每个线程创建了一个Map对象,每个Map的value就是存入的messages本质是对象,也就是T--ThreadLocalTest对象们,并且它们Map中的Entry中的Key值都是一样的,都是这个ThreadLocal,也就是holder。
【注】并不是每个线程的Map只能存放一个value对象,是这里我展示的例子里,一个线程只存了一条,完全可以存入很多条消息,然后add()时就会累加在Map已经创建好的Entry后面也就是:
当然既然是Map,存储Entry就涉及Hash了,这个以后再详谈。
二、ThreadLocal.set()
源码流程图

源码解析
/**
* 设置当前线程的ThreadLocal变量值
* @param value 要存储的值,将保存在当前线程的ThreadLocal副本中
*/
public void set(T value) {
// 1. 委托给内部set方法,传入当前线程和值
set(Thread.currentThread(), value);
// 2. 如果启用了虚拟线程追踪标志,进行堆栈转储
if (TRACE_VTHREAD_LOCALS) {
dumpStackIfVirtualThread();
}
}
/**
* 设置载体线程的ThreadLocal变量值(专用于虚拟线程场景)
* @param value 要存储的值,将保存在载体线程的ThreadLocal副本中
*/
void setCarrierThreadLocal(T value) {
// 1. 断言当前对象必须是CarrierThreadLocal类型
assert this instanceof CarrierThreadLocal<T>;
// 2. 委托给内部set方法,传入当前载体线程和值
set(Thread.currentCarrierThread(), value);
}
/**
* 内部set方法,实际执行存储操作
* @param t 目标线程对象
* @param value 要存储的值
*/
private void set(Thread t, T value) {
// 1. 获取目标线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 2. 如果map已存在
if (map != null) {
// 2.1 直接设置键值对(以当前ThreadLocal实例为key)
map.set(this, value);
} else {
// 3. 如果map不存在,创建新map并存入初始值
createMap(t, value);
}
}
/**
* 获取线程的ThreadLocalMap(实际是Thread类的threadLocals字段)
* @param t 目标线程对象
* @return 线程的ThreadLocalMap对象
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 直接返回线程对象的成员变量
}
/**
* 创建线程的ThreadLocalMap并存入第一个值
* @param t 目标线程对象
* @param firstValue 要存储的第一个值
*/
void createMap(Thread t, T firstValue) {
// 创建新的ThreadLocalMap,以当前ThreadLocal为key,firstValue为值
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 虚拟线程调试方法:如果是虚拟线程则转储堆栈
*/
private void dumpStackIfVirtualThread() {
// 调试用方法,当TRACE_VTHREAD_LOCALS为true时调用
if (Thread.currentThread().isVirtual()) {
new Exception("VirtualThreadLocal set").printStackTrace();
}
}
思考
看过源码,尤其是流程图后,会感觉set和get好像十分相像,都是会先判断有没有Map,没有就初始化一个新的,然后调用get时会通过withInitial获取一个ThreadLocalTest实例,放在一个Entry中,默认key是Thread Local,然后value是ThreadLocalTest,也就是message。
那么究竟有什么区别呢?或是换个问题,为什么我们在例子中add()方法中使用get,而不是set方法呢?这就涉及到它们的核心源码了:
// get() 的逻辑(保留旧值)
if (map != null) {
Entry e = map.getEntry(this);
if (e != null) {
return (T)e.value; // 直接返回,不修改
}
}
return setInitialValue(); // 懒初始化
==========================================
// set() 的逻辑(覆盖旧值)
if (map != null) {
map.set(this, value); // 强制更新
} else {
createMap(t, value); // 初始化新Map
}
总结而言:
| 行为 | get() | set() |
|---|---|---|
| 对已有值的处理 | 保留旧值,直接返回 | 覆盖旧值 |
| 初始化值的来源 | 通过 withInitial 的 Supplier | 使用传入的新值 |
| 典型用途 | 获取并操作线程本地变量 | 强制更新线程本地变量 |
| 内存泄漏风险 | 无 | 有(需手动清理旧值) |
就可以形象理解为:
| 操作 | 类比现实场景 | 结果 |
|---|---|---|
get() | 打开抽屉,取出笔记本继续写笔记 | 笔记内容不断累积 |
set() | 扔掉旧笔记本,换一本新的空本子 | 旧笔记丢失,只能写新内容 |
三、ThreadLocal.remove()
流程
调用remove()
↓
获取当前线程的ThreadLocalMap
↓
找到对应ThreadLocal的Entry
↓
清除Entry的Key弱引用
↓
expungeStaleEntry():
1. 清理当前槽位
2. 向后探测清理过期Entry
3. 重新哈希非过期Entry
↓
减少Map的size计数
源码解析
/**
* 移除当前线程的ThreadLocal变量值。此操作会:
* 1. 清除当前ThreadLocal实例关联的Entry
* 2. 探测式清理哈希表中其他过期的Entry
* 3. 避免内存泄漏(关键方法)
*/
public void remove() {
// 1. 获取当前线程的ThreadLocalMap(可能为null)
ThreadLocalMap m = getMap(Thread.currentThread());
// 2. 如果Map存在,则移除当前ThreadLocal对应的Entry
if (m != null) {
m.remove(this); // this指当前ThreadLocal实例
}
}
/**
* ThreadLocalMap内部移除Entry的核心方法
* @param key 要移除的ThreadLocal实例(弱引用Key)
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 1. 计算初始哈希槽位置
int i = key.threadLocalHashCode & (len-1);
// 2. 线性探测查找目标Entry
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) { // 匹配Key
// 3. 清除Key的弱引用
e.clear(); // 调用WeakReference.clear()
// 4. 探测式清理过期Entry(关键!)
expungeStaleEntry(i);
return;
}
}
}
/**
* 探测式清理过期Entry(扩容和读取时也会触发此逻辑)
* @param staleSlot 已知的过期Entry位置
* @return 返回下一个可能为空的槽位索引
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 1. 清理当前槽位的过期Entry
tab[staleSlot].value = null; // 释放Value强引用
tab[staleSlot] = null; // 清空槽位
size--; // 更新大小
// 2. 向后遍历进行探测式清理
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 2.1 发现其他过期Entry,一并清理
e.value = null;
tab[i] = null;
size--;
} else {
// 2.2 对非过期Entry重新哈希
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // 如果不在最佳位置
tab[i] = null; // 清空当前位置
// 2.3 将Entry移动到更接近最佳位置的空槽
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* 清除Entry的Key引用(继承自WeakReference)
*/
public void clear() {
super.clear(); // 将referent(Key)置为null
}
/**
* 获取下一个哈希槽位置(线性探测法)
* @param i 当前索引
* @param len table长度
* @return 下一个索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0); // 环形探测
}
四、ThreadLocal的内存泄露
在讲解ThreadLocal的内存泄漏前,先来讲解一下内存泄漏和内存溢出:
| 特征 | 内存泄漏 | 内存溢出 |
|---|---|---|
| 本质 | 内存未释放,逐渐积累 | 内存申请超过系统上限 |
| 过程 | 渐进式(长时间运行后暴露) | 突发性(立即或短时间内) |
| 结果 | 可能最终导致溢出 | 直接导致程序崩溃 |
| 解决方向 | 找到未释放的引用或资源 | 减少内存占用或增加系统内存 |
下图是ThreadLocal相关的内存结构图,在栈区中有threadLocal对象和当前线程对象,分别指向堆区真正存储的类对象,这俩个指向都是强引用。在堆区中当前线程肯定是只有自己的Map的信息的,而Map中又存储着一个个的Entry节点;在Entry节点中每一个Key都是ThreadLocal的实例,同时Value又指向了真正的存储的数据位置,以上便是下图的引用关系。

延伸--Java的四种引用类型
- 强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
- 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
- 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
ThreadLocal中Entry的设定:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // 实际存储的值(强引用)
Entry(ThreadLocal<?> k, Object v) {
super(k); // Key 是弱引用
value = v;
}
}
//所以可以看出,严格意义来说,key这里存储的是ThreadLocal的弱引用
键(Key):
ThreadLocal实例(弱引用,防止内存泄漏)。值(Value):线程局部变量(强引用,需手动清理)。
发生内存泄露的场景
①ThreadLocal被回收(没有外部强引用):例如:
ThreadLocal tl = new ThreadLocal(); tl = null;(tl被 GC 回收)。②线程未结束(如线程池中的线程长期存活):
ThreadLocalMap仍然持有Entry,但Entry的key(ThreadLocal)已经是null,而value仍然占用内存。③未调用
remove():如果业务代码没有显式调用
ThreadLocal.remove(),value会一直存在,导致内存泄漏。

1万+

被折叠的 条评论
为什么被折叠?



