Java 中ThreadLocal.ThreadLocalMap是ThreadLocal机制内部用来存储每个线程私有变量的数据结构。下面我将从整体设计、哈希冲突处理、内存泄漏防护、以及流程模拟四个方面进行详细解析,并在最后给出一张内存结构图和一次 get/set 流程示例。
一、整体设计思想
1.1 ThreadLocalMap 是什么?
- 它是一个定制化的哈希表(hash map),只用于保存当前线程的
ThreadLocal变量。 - 不对外暴露任何操作接口(包私有),仅由
ThreadLocal类使用。 - 每个
Thread对象内部持有一个ThreadLocalMap实例(字段名为threadLocals)。
1.2 Entry 结构
staticclassEntryextendsWeakReference<ThreadLocal<?>>{Objectvalue;}- Key 是
ThreadLocal对象本身,但被包装成弱引用(WeakReference)。 - Value 是用户存入的实际数据。
- 当某个
ThreadLocal对象没有强引用指向它时(即用户不再持有该ThreadLocal实例),GC 会回收 key,此时 entry 的 key 变为null,称为stale entry(陈旧条目)。
⚠️ 注意:由于 key 是弱引用,value 不会被自动回收,若不清理 stale entry,会导致内存泄漏。
二、如何解决哈希冲突?
2.1 哈希函数
inti=key.threadLocalHashCode&(table.length-1);- 使用
ThreadLocal自带的threadLocalHashCode(一个固定、均匀分布的 long 值)。 - 表长度始终是2 的幂,所以用
& (len - 1)等价于取模,效率高。
2.2 冲突解决策略:线性探测(Linear Probing)
- 当目标槽位已被占用(无论是否 stale),就顺序向后查找下一个空槽:
privatestaticintnextIndex(inti,intlen){return((i+1<len)?i+1:0);// 循环回到开头} - 这是一种开放寻址法(Open Addressing),不是链表或红黑树。
✅ 优点:缓存友好(局部性好)
❌ 缺点:容易聚集(clustering),需配合清理 stale entry
三、内存泄漏防护机制
3.1 为什么会有内存泄漏?
- Key 是弱引用 → GC 后 key = null
- 但 Entry 本身还在 table 中 → value 仍被强引用 → 无法回收
3.2 如何清理 stale entry?
(1)expungeStaleEntry(int staleSlot)
- 从
staleSlot开始,向后扫描直到遇到 null 槽 - 删除所有 key == null 的 entry
- 对非 stale entry重新 rehash 插入(因为线性探测依赖连续性)
(2)cleanSomeSlots(int i, int n)
- 启发式扫描:对数级别(log n)地检查部分槽位
- 在
set()成功插入新元素后调用,平衡性能与清理效果
(3)replaceStaleEntry(...)
- 在
set()时如果遇到 stale entry,不仅替换,还顺带清理整个“run”(连续非空段)
(4)rehash()
- 先全表清理 stale entries
- 若 size 仍 ≥ 3/4 * threshold,则 resize(扩容为 2 倍)
📌 关键点:不使用 ReferenceQueue,所以 stale entry不会自动通知,只能靠主动探测清理。
四、get / set 流程详解(附模拟)
场景设定:
- 初始
ThreadLocalMap容量 = 16 - 存入两个
ThreadLocal:tl1,tl2 - 假设
tl1.threadLocalHashCode & 15 = 3 tl2.threadLocalHashCode & 15 = 3→哈希冲突!
✅ set(tl1, “A”) 流程:
- 计算索引:
i = 3 table[3] == null→ 直接放入new Entry(tl1, "A")size = 1- 调用
cleanSomeSlots(3, 1)→ 扫描约 log₂(1)=0 次,基本不扫 - 结束
index: 0 1 2 [3] 4 ... 15 . . . [tl1→"A"] ...✅ set(tl2, “B”) 流程(冲突发生):
- 计算索引:
i = 3 table[3] != null,且e.get() == tl1 ≠ tl2- 进入 for 循环:
i = nextIndex(3,16) = 4 table[4] == null→ 退出循环table[4] = new Entry(tl2, "B")size = 2- 调用
cleanSomeSlots(4, 2)→ 扫描 log₂(2)=1 次(检查 index 5) - 若无 stale,结束
index: 0 1 2 [3] [4] 5 ... . . . [tl1→A] [tl2→B] ...✅ get(tl2) 流程:
- 计算索引:
i = 3 table[3] != null,但e.get() == tl1 ≠ tl2→miss- 调用
getEntryAfterMiss(tl2, 3, table[3])- 进入 while 循环:
- i=3: k=tl1 ≠ tl2 → i=4
- i=4: e != null, k=tl2 == key →return e
- 进入 while 循环:
- 返回
"B"
❌ 内存泄漏场景(未 remove):
假设之后tl2 = null;(用户丢弃引用)
- GC 后,
table[4].get() == null→ stale entry - 若长期不调用
set/get/remove,该 entry 不会被清理 →"B"一直占用内存
✅ 正确做法:使用完 ThreadLocal 后调用tl.remove()
五、ThreadLocal 内存结构图
+---------------------+ | Thread | | | | threadLocals ─────┼───► ThreadLocalMap | | +------------------+ +---------------------+ | table (Entry[]) | | [0] | | [1] | | ... | | [3] ──► Entry ──► { key: WeakRef(tl1), value: "A" } | [4] ──► Entry ──► { key: WeakRef(tl2), value: "B" } | ... | +------------------+ Entry 继承 WeakReference<ThreadLocal> └── key 通过 get() 获取(可能为 null) └── value 是强引用(需手动清理)🔁 每个线程有自己的
ThreadLocalMap,互不干扰 →线程隔离
六、总结关键点
| 特性 | 说明 |
|---|---|
| 哈希冲突 | 线性探测(开放寻址) |
| Key 引用类型 | WeakReference(防 ThreadLocal 本身泄漏) |
| Value 生命周期 | 依赖 stale entry 清理,否则内存泄漏 |
| 清理时机 | get/set/remove 时触发 expunge/cleanSomeSlots/rehash |
| 扩容条件 | size ≥ 2/3 * capacity,且清理后仍 ≥ 3/4 * threshold |
| 最佳实践 | 使用完务必remove() |
如有需要,我可以进一步画出ASCII 流程图或UML 结构图。希望这份详解能帮你彻底理解ThreadLocalMap的精妙设计!