Thread Local的学习笔记
在多线程编程中,如何安全、简洁地为每个线程维护独立的变量副本,是开发者经常面临的挑战。Java 提供的 ThreadLocal 能够让我们轻松地在不同线程间隔离数据,避免了使用 synchronized 或复杂上下文传递的麻烦。本文将从使用方法、底层实现、常见陷阱与最佳实践几方面,深度剖析 ThreadLocal 的原理与应用,帮助你在日常开发中驾轻就熟地运用它。
一、Thread Local怎么使用?
线程局部变量
是一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离。每个线程各用各的
怎么使用Thread Local?
创建Thread Local
//创建一个ThreadLocal变量 public static ThreadLocal<String> localVariable = new ThreadLocal<>();设置Thread Local的值
//设置ThreadLocal变量的值 localVariable.set("天下万般兵刃,唯有过往伤人最深");获取Thread Local的值
//获取ThreadLocal变量的值 String value = localVariable.get();删除Thread Local的值
//删除ThreadLocal变量的值 localVariable.remove();记得用完要及时删除啊。至于原因,往下看你慢慢就知道了。
OK,到了这里,Thread Local的基本使用你就已经学会了。往下看就是更深入的一些知识了。当然,个人水平有限,也是一个初学者,这个博客也只是记录了自己的一个学习过程。如有错误还请毫不留情的指出来。
与普通的变量对比:
graph LR A[普通变量] --> B[多线程共享] --> C[需要同步控制] D[ThreadLocal变量] --> E[线程私有] --> F[无锁并发]每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 Thread Local 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。
二、底层实现原理深度解析
- Thread:每个线程持有
ThreadLocal.ThreadLocalMap实例 - Thread Local:提供
get()/set()/remove()等操作方法 - Thread Local Map:自定义哈希表,使用弱引用键(Entry extends Weak Reference)
每个 Thread 实例持有一个由 Thread Local 定义的 Thread Local Map 容器,Thread Local 通过自身实例作为键,在当前线程的 Thread Local Map 中存储 / 获取数据,实现线程间数据隔离。 上图:

下面上一些Thread Local的源码看看
get方法
/** * 返回当前线程对应的值;若不存在,则调用 setInitialValue 初始化。 */ public T get() { Thread t = Thread.currentThread();//这里的currentThread就是获取当前正在执行代码的线程对象的引用 ThreadLocalMap map = getMap(t);//然后这里拿到这个线程的Thread Local Map if (map != null) {//判断Map是否存在(初始时线程的threadLocals字段为null,即Map未创建) ThreadLocalMap.Entry e = map.getEntry(this);//这里的this指向的是Thread Local实例对象,作为key去拿entry if (e != null) { @SuppressWarnings("unchecked") T result = (T) e.value; return result;//拿到结果,return回去 } } return setInitialValue();//这里是第一次,get的时候map为null走的逻辑,也就是初始化一下这个线程的map } private T setInitialValue() { T value = initialValue();//null//这里调用的就是下面的initialValue,拿到一个null Thread t = Thread.currentThread();//获取当前正在执行代码的线程对象的引用 ThreadLocalMap map = getMap(t);//这里拿到这个线程的Thread Local Map 一开始确实是null哈 if (map != null) map.set(this, value);// 若ThreadLocalMap已存在,将当前 ThreadLocal实例作为键this,初始值作为值,存入 map else createMap(t, value);//创建了一个map return value; } protected T initialValue() { return null; } /** * 通过 Unsafe 在 Thread 对象上直接写入 threadLocals 字段 */ private void createMap(Thread t, T firstValue) { ThreadLocalMap m = new ThreadLocalMap(this, firstValue); UNSAFE.putObject(t, threadLocalsOffset, m); }set方法 以Thread Local实例对象为key,value为值,去Thread Local map里面设值
/** * 设置当前线程对应的值 * @param value 要设置的值 */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
接下来是Thread Local里面的静态类Thread Local Map的源码分析 🙂
Thread Local Map 中的每个 Entry 以弱引用包装 Thread Local 实例作为键,以强引用存储用户设置的值,弱引用键允许 Thread Local 实例在无强引用时被回收,结合主动清理机制避免内存泄漏。什么是强软弱虚引用后续再说
这张图很形象啊,图来自二哥 Thread → Thread Local Map → Entry(Key=Weak Ref

/* ———— 内部静态类:ThreadLocalMap ———— */
/**
* 每个线程持有一个 ThreadLocalMap,通过弱引用存储 ThreadLocal 键
*/
static class ThreadLocalMap { //ThreadLocalMap里面装的是一个个Entry对象,key是弱引用,value是强引用
/**
* map 中的条目:弱引用 key + 强引用 value
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/** 初始容量,必须为 2 的幂 */
private static final int INITIAL_CAPACITY = 16;
/** 存储弱引用 Entry 的数组 */
private Entry[] table;
/** 当前键值对数量 */
private int size = 0;
/** 重哈希阈值:当 size 超过 threshold 时扩容 */
private int threshold;
/**
* 构造:首次插入
* @param firstKey ThreadLocal 实例
* @param firstValue 对应值
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
threshold = INITIAL_CAPACITY * 2 / 3;
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
}
/*----------------------比较重要的一些方法----------------------*/
/**
* 根据 ThreadLocal 实例(键)查找对应的 Entry(开放地址法 + 线性探测)
*/
private Entry getEntry(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算初始哈希索引(利用 threadLocalHashCode 保证不同实例哈希值均匀分布)
int i = key.threadLocalHashCode & (len - 1);
Entry e = tab[i];
while (e != null && e.get() != key) {
i = nextIndex(i, len);
e = tab[i];
}
return e;
}
/**
* 插入或更新键值对,同时清理过期弱引用条目并处理扩容。
*aram key ThreadLocal 实例(键)
* @param value 用户存储的值(强引用)
*这里的set方法就是上面的public set方法中最终调用的set方法
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value; // 找到则更新
return;
}
if (k == null) {
// 旧弱引用:清理并重用槽位
replaceStaleEntry(key, value, i);
return;
}
}
// 未找到,插入新 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}三、Thread Local的内存泄露怎么回事?
通过前面的阅读,我们可以知道:Thread Local Map 的 Key是弱引用,但Value是强引用。 Thread Local Map使用Thread Local的弱引用作为key,如果一个Thread Local没有外部强引用引用它,那么系统GC的时候,这个Thread Local势必会被回收,这样一来,Thread local Map中就会出现key为null的Entry,既然key为null,那你就访问不到了,如果当前线程迟迟不能结束(比如你正好用的线程池)这些key为null的Entry的value就会一直存在一条强引用。
怎么解决呢?
使用完 Thread Local 后,及时调用 remove() 方法释放内存空间。
try {
threadLocal.set(value);
// 执行业务操作
} finally {
threadLocal.remove(); // 确保能够执行清理
}remove() 方法会将当前线程的 Thread Local Map 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算 key 的 hash 值
int i = key.threadLocalHashCode & (len-1);
// 遍历数组,找到 key 为 null 的 Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 将 key 为 null 的 Entry 清除
e.clear();
expungeStaleEntry(i);
return;
}
}
}
public void clear() {
this.referent = null;
}Thread Local Map的扩容机制
- 扩容触发条件:
- 在
ThreadLocalMap中,当size(当前有效键值对数量)超过threshold(阈值)时,会触发扩容。threshold的初始值为INITIAL_CAPACITY(默认 16)的 2/3,即threshold = INITIAL_CAPACITY * 2 / 3。
- 在
- 扩容过程:
- 创建新数组:扩容时,会创建一个新的
Entry数组,新数组的长度是原来的 2 倍。例如,原来的容量是 16,扩容后变为 32。 - 重新计算索引并迁移数据:遍历旧数组中的每个
Entry,对于每个非空的Entry,重新计算其在新数组中的索引位置。计算索引的方式是使用ThreadLocal实例的threadLocalHashCode与新数组长度减 1 进行按位与操作(h = k.threadLocalHashCode & (newLen - 1))。如果新位置已经被占用,则通过线性探测(while (newTab[h] != null) h = nextIndex(h, newLen);)找到下一个可用的位置,然后将Entry放入新数组。 - 更新相关参数:扩容后,
threshold会更新为新容量的 2/3,table会指向新的数组。
- 创建新数组:扩容时,会创建一个新的
- 扩容的意义:扩容可以减少哈希冲突,提高
ThreadLocalMap的查找效率。当哈希表中的元素越来越多,哈希冲突的概率会增加,通过扩容可以重新分配元素的存储位置,使元素分布更加均匀。
四、Thread Local的应用场景
// Web 应用中为每个请求绑定用户上下文
private static final ThreadLocal<UserContext> userContext = ThreadLocal.withInitial(UserContext::new);
public void handleRequest(Request req) {
try {
userContext.get().setUserId(req.getParameter("userId"));
// 业务逻辑
} finally {
userContext.remove();
}
}