👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中... 博客更新速度++
👍 欢迎点赞、收藏、关注,跟上我的更新节奏
📚欢迎订阅专栏,专栏名《在2B工作中寻求并发是否搞错了什么》
前言
经过了上一篇的学习,聪明的你一定知道了ThradLocal的怎么样使用的。
下面,跟上主播的节奏,马上开始ThreadLocal源码的阅读( ̄▽ ̄)"
内部结构
如下图所示,我们可以知道,每个线程,都有自己的threadLocals
字段,指向ThreadLocalMap
。
ThreadLocalMap
中有一个Entry
数组(table),用来存储我们set进ThreadLocal的值。
Entry
的key指向ThreadLocal(弱引用),value就是我们set的值(强引用)。

Set流程

java
// set方法入口
public void set(T value) {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 设置值
map.set(this, value);
} else {
// 为当前线程创建ThradLocalMap
createMap(t, value);
}
}
// getMap方法。获取当前线程,ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
map为null的情况,开始初始化
初始化
java
void createMap(Thread t, T firstValue) {
// 为当前线程设置ThreadLocalMap
// key是TheadLocal,value是我们要塞入ThreadLocal线程的值
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
具体new的逻辑
java
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 设置table大小,初始容量为16。ThreadLocalMap的table就是用来存Entry的。
table = new Entry[INITIAL_CAPACITY];
// 哈希算法算法,决定新的Entry插入到哪个槽里,后面会具体说这个。
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
// 当前在ThreadLocalMap中的Entry数量
size = 1;
// 设置扩容的阈值,2/3的时候扩容
setThreshold(INITIAL_CAPACITY);
}
初始容量,16个,强制要求为2的幂次,用于优化位运算性能。通过静态final修饰确保全局唯一且不可修改。
java
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
扩容阈值,2/3 的时候扩容
java
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
好了,让我们具体来说说说是怎么哈希的
java
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
为什么是 &? firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)?
当容量 INITIAL_CAPACITY
是 2 的幂(如 16)时,INITIAL_CAPACITY - 1
的二进制形式为全 1(例如 15 -> 1111
)。此时,hash & (INITIAL_CAPACITY - 1)
等效于 hash % INITIAL_CAPACITY
,但位运算的效率远高于取模运算。
firstKey.threadLocalHashCode,这个咋来的?
java
private final int threadLocalHashCode = nextHashCode();
// nextHashCode方法
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// nextHashCode,从0开始的hashCode
private static AtomicInteger nextHashCode = new AtomicInteger();
// HASH_INCREMENT
private static final int HASH_INCREMENT = 0x61c88647;
为什么这个魔数是0x61c88647?
0x61c88647
近似于 (√5 - 1)/2 * 2^32
(黄金分割比例的 32 位扩展)。这种设计确保哈希码在 2 的幂容量下均匀分布,减少冲突概率。
map不为null的情况,直接set
java
private void set(ThreadLocal<?> key, Object value) {
// ThreadLocalMap的table,table是个数组里面是放Entry的
Entry[] tab = table;
int len = tab.length
// hash到哪个槽位(上面初始化的时候,具体有说)
int i = key.threadLocalHashCode & (len-1);
// 判断当前的槽是否已经有Entry了
// - 如果有:就for循环table,看是不是这次要更新的槽。
// - 是的话更新。
// - 不是的话,说明存在哈希冲突,需要通过开放地址法,找到为空的槽插入。
// - 如果没有:就不执行下面的for循环,直接插入即可。
// ===== ThreadLocal哈希冲突解决 =====
// 我们都知道,这里的哈希冲突解决方案是 ---》 开放地址法(线性探测法)
// 这里的处理:如果enty不为null的话,就一直i+1往下找下一个,直到table[i] == null。
//(下面有nextIndex的代码,就是i+1。)
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果当前Entry的key,就是这次set需要的Entry的key,这里就更新
if (k == key) {
e.value = value;
return;
}
// 如果这个Entry的key为null(可能是线程执行完被GC了)
if (k == null) {
// 清除这个Entry(后面会具体说怎么清除的)
replaceStaleEntry(key, value, i);
return;
}
}
// 对该槽赋值
tab[i] = new Entry(key, value);
// table中Entry数量+1
int sz = ++size;
// 1、cleanSomeSlots清理没用的槽
// 2、如果清空失败 并且 table中的Entry数量大于等于扩容阈值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 重新hash
}
寻找下一个地址的方法nextIndex。
java
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
rehash做了什么?
java
private void rehash() {
// 清除过期的Entry(Entry的key为null就是过期的意思,后面会说具体说怎么清理的)
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// 通过主动提前扩容,防止哈希表在临界负载时因性能骤降影响用户体验
// 当前Entry的数量 大于等于 扩容阈值的1/2
if (size >= threshold - threshold / 4)
resize();
}
为什么是threshold - threshold / 4
?
因为要保持低附载。所以我们也可以说这是1 / 2扩容。
在开放寻址法中,通过主动降低扩容阈值(从 2/3
容量降至 3/4 * 2/3 = 1/2
容量),确保哈希表始终处于低负载状态,从而:减少线性探测的冲突概率,平滑性能波动,避免滞后现象,以可控的内存增长换取稳定的高性能。
那resize方法做了什么捏?省流版:扩容2倍扩容,旧的table里的Entry,重新哈希放入新的table里。
java
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 2倍当前长度大小
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍历旧table里的Entry,将Entry重新哈希,放入到新的table里
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// 清理失效的Entry
if (k == null) {
e.value = null;
} else {
// 哈希到新的槽位
int h = k.threadLocalHashCode & (newLen - 1);
// 开放地址法解决哈希冲突,如果这个节点有值了,就i+1找下一个槽位
while (newTab[h] != null)
h = nextIndex(h, newLen); // h+1,寻找下一个槽位
// 新table里,节点的赋值
newTab[h] = e;
// 旧table的有效Entry数量 + 1
count++;
}
}
}
// 设置新的扩容阈值(数组长度的 2/3)
setThreshold(newLen);
// 修改为新的table的size
size = count;
// 指向新的table
table = newTab;
}
// 设置新的扩容阈值
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
Get流程

get方法入口
java
public T get() {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 根据当前线程的ThreadLocal获取Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 当前线程没有ThreadLocalMap
return setInitialValue();
}
获取Entry,getEntry
方法。
java
private Entry getEntry(ThreadLocal<?> key) {
// 获取槽位
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果这个槽不为null 且 这个槽位Entry的key是当前的ThreadLocal
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e); // 当前槽找不到这次要获取的key
}
如果找不到,执行getEntryAfterMiss方法。
为什么会找不到?
可能是真没有,也有可能是哈希冲突导致的槽位一直++,所以Entry被放入到了后面。
java
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到了返回
if (k == key)
return e;
// 清除过期的Entry
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len); // 获取下一个槽位
// e变为下一个Entry
e = tab[i];
}
// 实在找不到
return null;
}
我们来看看,如果当前线程没有ThreadLocalMap,会怎么处理setInitialValue
方法
java
private T setInitialValue() {
// 初始value值,默认为null。子类可以重写,返回不同的初始值。
T value = initialValue();
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocal。(上面有说过这个方法,就是t.threadLocals。)
ThreadLocalMap map = getMap(t);
// 这个和的set流程一样了。有就set初始值进去,没有就创建map。
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
// 返回初始值
return value;
}
initialValue,默认返回null,子类可以重写。
java
protected T initialValue() {
return null;
}
下面,主播为大家表演一手,子类重写。
java
// 子类重写
public class MyThreadLocal extends ThreadLocal {
@Override
protected Object initialValue() {
return "aaaa";
}
}
// 测试类
public class MyThreadLocalTest {
public static void main(String[] args) {
MyThreadLocal myThreadLocalTest = new MyThreadLocal();
String o = (String) myThreadLocalTest.get();
System.out.println(o); // 输出aaaa
}
}
删除Entry
在ThreadLocal中存在2种删除方法。
探测删除(线性探测清理)
-
对应方法 :
expungeStaleEntry(int staleSlot)
-
核心目标:清理当前过期的 Entry,并重新整理哈希表,解决因哈希冲突导致的 Entry 位置偏移问题。
-
省流版流程:做了2件事情。
- 清空当前Entry 。
- 判断后续Entry是否需要清除,需要就清除,不需要的话,就重新哈希放到table里。
java
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理当前槽点的数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 从当前槽点开始向后遍历,判断每个Entry是否需要清空
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 失效的Entry,就给它清除
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// ====没有失效的Entry====
// 重新哈希槽位,Entry重新放入table中
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// 哈希冲突,开放址法
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// 返回停止的槽位
return i;
}
⚠️ 探测性修复是有局限性的,并不会从当前要删除的槽点一直往后遍历到table的所有槽点,它也会在某个为null的槽点中停下来。(🤔不过话又说回来了,真扫了整个table,性能包炸的)
java
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null; // 当tab[i]为null会停止清除,即使tab[i+1]是过期的Entry
i = nextIndex(i, len))
启发式清除(概率性扫描)
对应方法 :cleanSomeSlots(int i, int n)
核心目标:以较低的成本扫描部分槽位,清理可能的过期 Entry,避免全表扫描的性能开销。
java
// 入参解释:
// - i,一个为null的槽位。
// - n,一般传的是table的长度。
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
// 一直修改len的值,如果len的默认值为16,16 -> 8 -> 4 -> 2 -> 0
do {
i = nextIndex(i, len);
Entry e = tab[i];
// 当前节点是过期的
if (e != null && e.get() == null) {
n = len;
removed = true;
// 调用探测删除方法
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
while ((n >>>= 1) != 0)
是一种高效且巧妙的设计:
- 位运算控制循环次数 :以
O(log n)
时间实现启发式扫描。 - 平衡性能与清理效果:避免全表扫描,但覆盖足够多的槽位。
- 自适应哈希表容量:天然适配 2 的幂次容量的哈希表。
Remove流程
上面介绍了Entry是怎么删除的,这里来看看,remove到底干了什么勒?😄
java
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()); // 获取当前线程的ThreadLocalMap
if (m != null)
m.remove(this);
}
具体的remove方法,我们总结下做了啥?
- 哈希开始的槽位,找到要删除的Entry
- 断开Entry对ThreadLocal key的弱引用(置为null)
- 探测式删除Entry。
java
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 获取开始的槽位
int i = key.threadLocalHashCode & (len-1);
// 可能出现哈希冲突,所以要一直向后找Entry。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear(); // 删除key的弱引用
expungeStaleEntry(i); // 上面介绍过勒,这个是探测式删除。
return;
}
}
}
为什么会内存泄漏?
这就不得不提到我们的Entry了,我们可以看到,我们的Key是一个弱引用。
java
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 调用父类 WeakReference 的构造函数,将 k 作为弱引用存储
value = v;
}
}
那么,弱引用会怎么样勒?来主播带大家回忆下,Java的四种引用:
强引用 (Strong Reference)
- 清除时机:当对象没有任何强引用指向时,会在下一次GC时被回收。即使内存不足,也不会被回收(可能导致OOM)。
java
Object obj = new Object(); // 强引用
软引用 (SoftReference)
- 清除时机:当内存不足时(即将抛出OutOfMemoryError前),JVM会尝试回收。
java
SoftReference<Object> softRef = new SoftReference<>(new Object());
弱引用 (WeakReference)
- 清除时机:只要发生垃圾回收,无论内存是否充足,对象都会被回收。
java
WeakReference<Object> weakRef = new WeakReference<>(new Object());
虚引用 (PhantomReference)
- 清除时机: 对象被回收后,虚引用会被放入关联的ReferenceQueue,需手动处理。无法通过虚引用获取对象实例,仅用于追踪对象回收状态。
java
// 1. 必须绑定引用队列
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object heavyObject = new Object(); // 假设这是一个占用大量资源的对象
// 2. 创建虚引用(必须关联队列)
PhantomReference<Object> phantomRef =
new PhantomReference<>(heavyObject, queue);
// 3. 关键特性:无法通过虚引用获取对象
System.out.println(phantomRef.get()); // 输出 null ❌
// 4. 手动解除强引用(触发回收条件)
heavyObject = null;
// 5. 强制触发垃圾回收
System.gc();
// 6. 检查队列:当对象被回收后,虚引用会被加入队列
Reference<?> ref = queue.remove(500); // 阻塞500ms等待队列
if (ref == phantomRef) {
System.out.println("对象已被回收,可执行后续清理操作 ✅");
// 例如:若对象管理了堆外内存,可在此释放
}
ok,前面铺垫了这么久,让我们分析下,内存泄漏的原因:
Key 的弱引用特性
- 当
ThreadLocal
实例失去强引用时 (例如threadLocal = null
),由于Entry
的 key 是弱引用,GC 会回收该ThreadLocal
实例,此时Entry
的 key 变为null
。 - 问题 :
Entry
的value
仍是强引用,无法被自动回收。

线程长期存活
如果线程是线程池的核心线程(长期存活),它的 ThreadLocalMap
会一直存在。即使 key
被回收,value
仍然通过 Entry
的强引用保留在内存中。
代码模拟,没有及时remove,导致OOM:
java
public class ThreadLocalOOMExample {
// 设置 JVM 参数:-Xmx64m -Xms64m (限制堆内存为 64MB)
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(5); // 线程池复用线程
for (int i = 0; i < 1000; i++) { // 提交 1000 个任务
executor.submit(() -> {
ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();
try {
// 每个任务创建一个 1MB 的对象,存到 ThreadLocal 中
threadLocal.set(new BigObject());
// 模拟业务逻辑(不调用 remove())
} finally {
// 此处故意不调用 threadLocal.remove()
}
});
Thread.sleep(10); // 控制任务提交速度
}
executor.shutdown();
}
static class BigObject {
// 每个对象占用 1MB 内存
private final byte[] data = new byte[1024 * 1024];
}
}
运行结果
shell
Exception in thread "pool-1-thread-2" java.lang.OutOfMemoryError: Java heap space
at ThreadLocalOOMExample$BigObject.<init>(ThreadLocalOOMExample.java:23)
at ThreadLocalOOMExample.lambda$main$0(ThreadLocalOOMExample.java:15)
...
后话
聪明的你,是不是对ThreadLocal有更多的了解呢?
- ThreadLocalMap的数据结构是怎么样
- value是怎么set进去
- 怎么get到我们set的value
- 如何删除过期的Entry
- 内存泄漏的可能原因
相信聪明的你一定有了答案(‾◡◝)