# 问题

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;
}
1
2
3
# 问题
  1. 锁粒度仍然较大
    • 线程访问不同 Segment 可以并行,但同一 Segment 内的所有 key 仍然竞争同一把锁
  2. 扩容过程复杂
    • 需要 ReentrantLock 依次锁住所有 Segment 进行扩容,影响吞吐量。
  3. 读操作可能仍然受锁限制
    • 由于 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;
}
1
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 加锁
}
1
2
3
4
5
6
# (2)红黑树优化高并发下的哈希冲突
  • 当链表长度超过 8,转换为 红黑树,减少链表遍历时间复杂度 O(n) → O(log n)。
  • 但红黑树 不会无限增长,避免存储开销过大,删除数据后可能恢复为链表。
if (binCount >= TREEIFY_THRESHOLD)
    treeifyBin(tab, i);
1
2
# (3)volatile 保证可见性
  • Node.valtable 采用 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));
1
2
3

# 4. JDK 1.7 vs JDK 1.8 并发优化对比

版本 并发控制 数据结构 读性能 写性能 扩容效率
JDK 1.7 Segment + ReentrantLock 数组 + Segment + 链表 可能受锁影响 受锁影响较大 逐个迁移,低效
JDK 1.8 CAS + Synchronized 数组 + 链表 / 红黑树 volatile 提高读性能 细粒度锁,提高写性能 并行扩容,高效

# 5. ConcurrentHashMap 的常见问题

常见误区

  1. 误认为 JDK 1.8 仍然使用 Segment

    • 事实:JDK 1.8 直接移除了 Segment,改为 Node[] + CAS + synchronized
  2. 误以为 put() 绝对无锁

    • 事实:首次插入使用 CAS,哈希冲突时会 synchronized 该 bin,但不会锁整个 Map。
  3. 忽视扩容带来的并发影响

    • 扩容时存在 ForwardingNode,但可能造成瞬时性能下降。

# 深入追问

🔹 为什么 JDK 1.8 不再使用 ReentrantLock 而是 synchronized
🔹 ConcurrentHashMap 如何避免扩容时的竞争?
🔹 为什么 putIfAbsent() 在高并发环境下仍然安全?
🔹 为什么要引入 ForwardingNode

# 相关面试题

ConcurrentHashMap 如何保证并发安全?
为什么 JDK 1.8 放弃 ReentrantLock
ConcurrentHashMap 和 SynchronizedMap 有什么区别?