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 锁桶头:桶非空时,只锁链表/红黑树的头节点。
三、为什么更好¶
| 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 层支持更好。