ThreadLocalMap 和 HashMap 有什么区别?

#Java#面试八股

ThreadLocalMap 和 HashMap 有什么区别?

结论先行:ThreadLocalMap 是 ThreadLocal 的专用私有结构,不是通用容器:它用开放寻址(线性探测)解决冲突、key 是弱引用的 ThreadLocal、阈值是 2/3、并且在读写路径上顺带清理脏 Entry。它甚至没有实现 Map 接口。

1. 逐项对比

维度ThreadLocalMapHashMap
归属Thread 的成员字段,随线程生灭独立对象,可自由传递
key 类型只能是 ThreadLocal,且是弱引用任意 Object,强引用
冲突解决开放寻址,线性探测 i+1拉链法,链表 + 红黑树(JDK 8)
初始容量1616
扩容阈值len * 2 / 3 ≈ 66%len * 0.75
扩容后双倍扩容 + rehash + 清理 key == null 的槽双倍扩容 + 链表拆分/树化
线程安全天然安全(线程私有,无锁)不安全,需外部同步
是否实现 Map否,ThreadLocal 的静态内部类是
迭代器无迭代器fail-fast 迭代器

2. 两种冲突解决方式

开放寻址(ThreadLocalMap):冲突就往后找空槽
  下标:  0     1     2     3     4
        [k1]  [k2]  [k3]  [空]  [k4]
                             ↑ k4 的 hash 落在 2,被占,线性探测到 3 空位

拉链法(HashMap):冲突挂到同一个桶的链表上
  下标:  0        1      2
        [k1]→[k5] [k2]  [k3]→[k4]→[k9]

为什么 ThreadLocalMap 选开放寻址?因为一个线程里的 ThreadLocal 数量通常个位数到几十个,数据量极小。开放寻址没有 Node 对象、没有指针跳转,数组连续内存对 CPU 缓存更友好;而拉链法要为每个 Entry 额外分配一个 Node,在小规模场景下纯属浪费。

3. 关键代码:探测与清理

// 线性探测查找(简化)
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) return e;
    return getEntryAfterMiss(key, i, e);   // 冲突:从 i 往后逐个比对
}

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;
        if (k == null) expungeStaleEntry(i);   // 顺手清理 key 已被回收的脏槽
        else i = nextIndex(i, len);            // i = (i + 1) & (len - 1)
        e = tab[i];
    }
    return null;
}

expungeStaleEntry 是这个结构最"聪明"的地方:它把 key == null 的槽清空,并把后续可能"探测错位"的元素往前挪,防止线性探测链断裂导致查不到数据。

4. 为什么它敢不实现 Map 接口

因为它根本不需要通用性:没有 putAll、没有迭代、没有 keySet,只有 getEntry、set、remove、rehash 四个内部操作。不实现接口反而少了一层虚方法调用,也避免了它被当成普通 Map 传来传去。

5. 常见追问

追问一:为什么阈值是 2/3 而不是 0.75? 开放寻址对负载因子更敏感:装载率越高,探测链越长,性能下降越陡。2/3 是在"内存占用"和"探测长度"之间的经验折中。阈值和容量都用 len * 2 / 3 这样的整数运算,避免浮点开销。

追问二:容量为什么必须是 2 的幂? 为了用 hash & (len - 1) 代替取模,同时保证线性探测 (i + 1) & (len - 1) 能绕回数组头部。这也是 HashMap 容量为 2 的幂的同一套理由。

追问三:ThreadLocalMap 会 OOM 吗? 会。它挂在 Thread 上,如果线程是线程池里的长命线程,且 ThreadLocal 数量不断增长(比如每个请求 new ThreadLocal),map 会一直扩容,且 value 无法回收,最终堆溢出。所以永远用 static final 声明 ThreadLocal,别在方法里 new。

追问四:为什么不直接复用 HashMap 加同步? 那样每次 get/set 都要锁,违背了 ThreadLocal"零竞争"的设计初衷;而且 HashMap 的 key 是强引用,ThreadLocal 对象无法被回收,泄漏只会更严重。

小结

ThreadLocalMap 是为"一个线程、少量 key、绝不共享"这个极端场景定制的迷你哈希表:开放寻址省内存、弱引用 key 防泄漏、探测路径顺手清脏数据。把它当 HashMap 用(比如指望它存几千个 key、或者跨线程访问)就完全跑偏了。

笔记加载中…