跳转至

Java 8 的 ConcurrentHashMap 为什么放弃分段锁?

一、JDK7 的分段锁

JDK7 的 ConcurrentHashMap 用 Segment[]

  • 每个 Segment 继承 ReentrantLock,是一个小 Hashtable。
  • 默认 16 个 Segment,并发度 16。
  • 写操作锁整个 Segment,读操作几乎无锁(volatile)。

二、JDK8 为什么放弃

1. 并发度受 Segment 数限制

默认 16,要更高并发就得调大 Segment 数,但每个 Segment 又是一个小 table,内存浪费。

2. 锁粒度还是太粗

一个 Segment 可能有多个桶,锁整个 Segment 仍然阻塞同段的其他桶。

3. JDK8 的新方案

放弃 Segment,改为:

  • 数组 + 链表 + 红黑树,结构和 HashMap 一样。
  • CAS:桶为空时,直接 CAS 插入,无锁。
  • synchronized 锁桶头:桶非空时,只锁链表/红黑树的头节点。
synchronized (f) {
    // f 是桶头节点,只锁这一个桶
}

三、为什么更好

JDK7 Segment JDK8 桶锁
锁粒度 一段(多个桶) 单个桶
并发度 Segment 数(默认 16) 桶数(默认 16,扩容后更多)
volatile volatile + 无锁
实现 ReentrantLock synchronized + CAS

JDK8 的并发度几乎等于桶数,理论上比 JDK7 高很多。

四、synchronized 为什么比 ReentrantLock 好

JDK6 后 synchronized 做了大量优化:偏向锁、轻量级锁、锁粗化、锁消除。在锁竞争不激烈时,synchronized 性能已经不逊于 ReentrantLock,而且更轻量。

五、size 怎么统计

JDK8 用 baseCount + CounterCell[] 分段计数,减少 CAS 冲突(类似 LongAdder)。

高频追问

  • 为什么不允许 null key/value?并发下 get 返回 null,无法区分"不存在"和"值是 null",会有二义性。
  • JDK8 ConcurrentHashMap 为什么用 synchronized 而不是 ReentrantLock?因为 synchronized 在 JDK6+ 优化后足够好,且 JVM 层支持更好。