ConcurrentHashMap 的分段锁被废弃是因为 Java 8 彻底重写为基于 Node 数组 + synchronized 单桶锁、CAS 和 volatile,解决了 Segment 锁粒度粗、内存高、扩容复杂等问题。

ConcurrentHashMap 的分段锁为什么被废弃了?
Java 8 及以后版本中,ConcurrentHashMap 已完全移除 Segment 分段锁机制,不是“优化”,而是彻底重写。旧版(Java 7)用 Segment 数组模拟多把锁,但存在锁粒度粗、内存占用高、扩容复杂等问题。JDK 8 改为基于 Node 数组 + synchronized 锁单个桶(bin),配合 volatile 读和 CAS 操作,既降低竞争又避免锁膨胀。
- Java 7 的
Segment实际是ReentrantLock子类,每个Segment管理多个桶,锁粒度仍偏大 - Java 8 中,只有在链表转红黑树、插入冲突、扩容等关键路径才加
synchronized,且只锁当前Node所在桶头节点 -
size()不再依赖全局锁或Segment计数,改用baseCount+CounterCell[]的 CAS 分段计数,但结果是最终一致而非强实时
get() 方法真的完全无锁吗?
是的,get() 在绝大多数情况下不加锁,但前提是数据未处于结构变更中(如正在扩容、树化)。它依赖 volatile 语义读取 Node.val 和 Node.next 字段,保证看到最新写入值;而数组本身用 Unsafe.getObjectVolatile() 读取,确保不会发生指令重排导致读到未初始化的桶。
- 如果遇到正在迁移的桶(
ForwardingNode),get()会直接去新表查找,不阻塞也不重试 - 若当前桶是红黑树节点(
TreeBin),则调用其find()方法——该方法内部也无需加锁,因为树结构在读期间不会被修改(写操作会先锁住TreeBin根节点) - 注意:
get()不保证看到其他线程刚put()但尚未完成 CAS 写入next字段的中间状态,这是 JMM 允许的正常现象
put() 过程中如何避免死循环?
Java 8 的 ConcurrentHashMap.put() 在链表遍历时使用 == 判断节点是否为自身(即 e == e),作为链表未损坏的快速校验。一旦发现 next 字段指向自己(e.next == e),说明链表已被其他线程标记为正在迁移(变成 ForwardingNode),立即跳出并协助扩容,而不是继续遍历造成无限循环。
for (Nodee = f;;) { if (e == null) { /* 插入 */ break; } else if (e.hash == hash && key.equals(e.key)) { /* 覆盖 */ break; } else if (e instanceof TreeBin) { /* 树操作 */ break; } else if (e == e.next) // 关键防护:检测自引用,避免死循环 break; e = e.next; }
- 这个
e == e.next判断不是“修复”手段,而是早期退出信号,后续逻辑会转向helpTransfer() - 红黑树操作由
TreeBin统一管理,其waiter字段和lockRoot()机制防止并发写导致结构错乱 - 所有 CAS 更新(如
tab[i]、node.next)都使用Unsafe的有序/延迟写入,避免因编译器或 CPU 重排引发可见性问题
为什么不能用 ConcurrentHashMap 替代 synchronized 块?
ConcurrentHashMap 是线程安全的容器,不是通用同步工具。它不提供跨操作的原子性保障,比如“检查是否存在再 put”(if (!map.containsKey(k)) map.put(k, v))仍是竞态的,必须用 computeIfAbsent() 或显式加锁。
立即学习“Java免费学习笔记(深入)”;
-
computeIfAbsent()是唯一推荐的“读-改-写”原子操作,其内部对 key 对应桶加锁,且只在 key 不存在时执行 mappingFunction -
replace(K, V, V)和remove(K, V)支持条件更新,依赖 CAS 比较旧值,但仅限单个 entry - 想实现类似
synchronized(map) { ... }的全局互斥?别这么做——它会严重退化性能,且违背ConcurrentHashMap的设计初衷 - 真正需要强一致性+复合逻辑时,应该考虑
StampedLock或外部协调机制,而不是强行塞进 map 操作里
实际用的时候,最常被忽略的是:它的弱一致性语义(尤其是迭代器)和 size() 的估算性质。如果你在高并发下靠 size() == 0 判断是否清空完毕,或者用 keySet().iterator() 遍历时假设能看见所有已提交的写入,就很容易出错。











