# 问题
6. ConcurrentHashMap 为什么放弃了 Segment 分段锁?如何保证高并发安全?
# 标准答案
JDK 1.8 之后,ConcurrentHashMap 放弃了 Segment 分段锁,改用 CAS + Synchronized + 红黑树 实现更细粒度的并发控制,提升了高并发场景下的性能。具体来说,它采用 Node 数组 + 链表/红黑树 结构,结合 CAS 更新、Synchronized 细粒度锁、volatile 保证可见性,避免了 JDK 1.7 中 Segment 级锁粒度过大、扩容复杂、读性能受限 的问题。
# 答案解析
# 1. JDK 1.7 的 Segment 分段锁问题
在 JDK 1.7 版本,ConcurrentHashMap 采用 Segment + HashEntry 结构:
- Segment 本质上是 ReentrantLock,每个
Segment维护一个哈希桶(类似HashMap)。 - 访问某个 Key 时,需要先定位
Segment,再在Segment内部查找。 put()需要获取Segment的锁,导致锁粒度仍然较大,多个 Key 可能因为哈希冲突落在同一个Segment,影响并发度。- 扩容时需要 对所有 Segment 依次加锁,并重新计算
hashCode(),导致扩容效率低。
static class Segment<K, V> extends ReentrantLock {
transient volatile HashEntry<K, V>[] table;
}
2
3
# 问题
- 锁粒度仍然较大
- 线程访问不同
Segment可以并行,但同一Segment内的所有 key 仍然竞争同一把锁。
- 线程访问不同
- 扩容过程复杂
- 需要
ReentrantLock依次锁住所有Segment进行扩容,影响吞吐量。
- 需要
- 读操作可能仍然受锁限制
- 由于
Segment需要保证数据一致性,某些情况下读操作仍然需要加锁,影响读性能。
- 由于
# 2. JDK 1.8 放弃 Segment,改用 CAS + Synchronized
在 JDK 1.8 之后,ConcurrentHashMap 采用了 更细粒度的锁控制,完全取消 Segment,核心结构变为:
- 数组 + 链表(或红黑树):底层仍然是
Node<K, V>[] table,索引位置i = hash & (table.length - 1) - CAS + Synchronized 代替
ReentrantLock,锁粒度细化到 单个桶(bin) - 红黑树优化高并发下的哈希冲突(当链表长度超过 8 时转为红黑树)
- 扩容优化:避免
hashCode()重新计算,提高性能
static class Node<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
2
3
4
5
6
# 3. JDK 1.8 如何保证高并发安全?
# (1)CAS + Synchronized 控制并发写
JDK 1.8 采用 CAS(compareAndSwap)+ Synchronized 取代 ReentrantLock,并发控制更精细:
- 初始化时,CAS 确保
table数组仅初始化一次,避免并发竞争。 - 插入第一个节点时,CAS 直接写入,避免锁竞争。
- 存在哈希冲突时,才对该桶(bin)加
synchronized进行加锁。
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, val, null))) {
// 通过 CAS 操作,避免并发冲突
}
synchronized (f) {
// 只在哈希冲突时对 bin 加锁
}
2
3
4
5
6
# (2)红黑树优化高并发下的哈希冲突
- 当链表长度超过 8,转换为 红黑树,减少链表遍历时间复杂度 O(n) → O(log n)。
- 但红黑树 不会无限增长,避免存储开销过大,删除数据后可能恢复为链表。
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
2
# (3)volatile 保证可见性
Node.val和table采用volatile修饰,确保线程修改数据后其他线程可见,避免ReentrantLock带来的性能损耗。
# (4)扩容优化
table数组满时,采用 多线程扩容,避免 JDK 1.7 单线程Segment依次扩容的问题。- 迁移数据时采用 CAS + 迁移标记(ForwardingNode),防止扩容过程中的并发竞争。
Node<K,V> f;
if ((f = tabAt(nextTab, i)) == null)
casTabAt(nextTab, i, null, new Node<K,V>(fh, null, null, null));
2
3
# 4. JDK 1.7 vs JDK 1.8 并发优化对比
| 版本 | 并发控制 | 数据结构 | 读性能 | 写性能 | 扩容效率 |
|---|---|---|---|---|---|
| JDK 1.7 | Segment + ReentrantLock | 数组 + Segment + 链表 | 可能受锁影响 | 受锁影响较大 | 逐个迁移,低效 |
| JDK 1.8 | CAS + Synchronized | 数组 + 链表 / 红黑树 | volatile 提高读性能 | 细粒度锁,提高写性能 | 并行扩容,高效 |
# 5. ConcurrentHashMap 的常见问题
❌ 常见误区
误认为 JDK 1.8 仍然使用 Segment
- 事实:JDK 1.8 直接移除了
Segment,改为Node[]+CAS + synchronized。
- 事实:JDK 1.8 直接移除了
误以为 put() 绝对无锁
- 事实:首次插入使用 CAS,哈希冲突时会 synchronized 该 bin,但不会锁整个 Map。
忽视扩容带来的并发影响
- 扩容时存在
ForwardingNode,但可能造成瞬时性能下降。
- 扩容时存在
# 深入追问
🔹 为什么 JDK 1.8 不再使用 ReentrantLock 而是 synchronized?
🔹 ConcurrentHashMap 如何避免扩容时的竞争?
🔹 为什么 putIfAbsent() 在高并发环境下仍然安全?
🔹 为什么要引入 ForwardingNode?
# 相关面试题
• ConcurrentHashMap 如何保证并发安全?
• 为什么 JDK 1.8 放弃 ReentrantLock?
• ConcurrentHashMap 和 SynchronizedMap 有什么区别?
