Java 面试题

ConcurrentHashMap底层原理:1.7分段锁 vs 1.8 CAS+synchronized

"ConcurrentHashMap是怎么做到线程安全的?1.7和1.8有什么区别?"

为什么面试官会问这道题

ConcurrentHashMap是并发数据结构的经典考题,HashMap的直接追问。1.7到1.8的演进专门考你对锁粒度的理解——分段锁和细粒度CAS之间的权衡。

如何回答

  1. 1

    JDK 1.7设计:Map分成16个Segment,每个Segment是小HashMap加一个ReentrantLock。不同Segment的写并行,并发度上限就是Segment数。

  2. 2

    JDK 1.8重写:废弃Segment,底层换成Node数组(类似HashMap),用CAS+synchronized实现锁粒度细到桶。空桶首次插入用CAS,非空桶synchronized锁桶头节点。

  3. 3

    get无锁:Node的val和next用volatile修饰,读操作直接遍历无需加锁,读吞吐非常高。

  4. 4

    put流程:算桶下标→桶为空时CAS插入头节点→桶非空时synchronized头节点→插入链表或红黑树。

  5. 5

    并发扩容:迁移时线程可以"互相帮忙"——其他put线程进来发现正在扩容,会被动员来协助迁移桶。迁移完的桶用ForwardingNode占位,读线程看到ForwardingNode会自动跳到新表。

参考回答示例

JDK 1.7的ConcurrentHashMap是分段锁设计。整个Map分成默认16个Segment,每个Segment是个小HashMap加一个ReentrantLock。写操作只锁一个Segment,不同Segment的写可以并行。并发度上限就是Segment数量,size()需要锁所有Segment(后来优化为先尝试不加锁累加再校验)。JDK 1.8彻底重写了。Segment没了,底层直接是Node数组像HashMap一样,每个桶是链表或红黑树。线程安全靠CAS+synchronized组合:桶为空时首次插入直接CAS把新Node设为头节点,不加锁;桶非空时synchronized锁住这个桶的头节点,再操作链表或树。不同桶完全独立,所以并发度从"16个Segment"升级到"桶数量"(默认16起步,扩容后可以到百万),粒度细得多。get()完全无锁——Node的val和next用volatile修饰,读操作直接遍历就行,多写多读场景下读性能极高。扩容也是并发的——put线程进来如果发现正在扩容,会被拉进来一起帮忙迁移桶。迁移完的桶替换成ForwardingNode标记,并发的读线程看到ForwardingNode会自动跳转到新表对应的桶去读,整个过程无缝衔接。size()在1.8用了LongAdder风格的分片计数器——每个线程更新自己的counter cell避免cache争用,size()时把所有cell加起来。这比1.7的全segment加锁快得多,尤其是写集中的场景。代价是代码变复杂了,synchronized锁桶头在热点桶上还是会有竞争,但整体比分段锁好一个数量级。

实用技巧

  • 1.7 vs 1.8的对比是面试真正考察点,两边都要讲。

  • 一定要说"CAS+synchronized",最常见的错误答案是"每个桶一个ReentrantLock"。

  • 讲到ForwardingNode是深读过源码的信号。

  • 忘了ForwardingNode或LongAdder,即答侠可以立即提示。

常见问题

为什么1.8用synchronized而不是ReentrantLock?

JDK 6之后synchronized有偏向锁/轻量级锁/重量级锁的升级机制,短临界区性能和ReentrantLock差不多,而且每个锁内存开销更小。

ConcurrentHashMap允许null key/value吗?

不允许。并发场景下null的语义模糊(get返回null分不清是不存在还是值就是null),所以禁止。

size()准确吗?

并发写时是估算值——不加锁累加counter cell,最终一致。严格场景不要依赖size()做判断。

面试时担心忘词?即答侠实时助你

即答侠 AI 实时监听面试对话,自动识别问题并即时生成回答建议——无感辅助,让你从容应对每一道题。

免费试用即答侠