# 问题
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 有什么区别?