Skip to Content
面试题库Juc[面试题] ConcurrentHashMap 是如何保证线程安全的?分段锁机制又是什么?后面为什么又要废除?

[面试题] ConcurrentHashMap 是如何保证线程安全的?分段锁机制又是什么?后面为什么又要废除?

📖 版块 A:知识讲解版 (温故知新)

  • JDK 1.7 的实现:分段锁(Segment Lock)

    1. 结构:由 Segment 数组和 HashEntry 数组组成。每个 Segment 继承自 ReentrantLock
    2. 原理:将数据分为多个段(默认 16 个),每把锁只锁住其中的一段数据。当多线程访问不同段的数据时,不存在锁竞争。
    3. 缺点:并发度在初始化时固定(由 concurrencyLevel 决定),后期无法扩容;每个 Segment 都是一个对象,内存消耗大。
  • JDK 1.8 的实现:CAS + synchronized

    1. 结构:摒弃了 Segment 数组,回归了类似 HashMap 的 Node 数组 + 链表/红黑树结构。

    2. 原理

      • CAS:在插入新节点时,如果桶(Bucket)为空,利用 CAS 操作无锁插入。
      • synchronized:如果桶不为空(产生哈希冲突),则锁住当前桶的头节点(Node)。
    3. 优势:锁粒度从“段”细化到了“桶”,并发度直接取决于数组长度;减少了内存开销。

  • 废除分段锁的原因

    1. 锁粒度不够细:Segment 级别锁还是太重,1.8 锁住头节点能支持更高的并发。
    2. 内存开销:Segment 继承 ReentrantLock,每个段都要维护状态、等待队列等,空间浪费严重。
    3. JVM 性能优化:JDK 6 之后 synchronized 引入了偏向锁、轻量级锁和锁消除等优化,性能已经不输于 ReentrantLock,且能减少代码复杂性。

🎤 版块 B:面试实战版 (高手逐字稿)

Q:ConcurrentHashMap 是如何保证线程安全的?分段锁机制又是什么?后面为什么又要废除?

💬 高手逐字稿

关于 ConcurrentHashMap 的线程安全机制,需要分 JDK 1.7 和 1.8 两个阶段来拆解。

JDK 1.7 中,它采用的是分段锁(Segment Locking)。其核心思想是“化整为零”。它把整个 Hash 表拆分成多个 Segment(段),每个 Segment 本质上是一个继承了 ReentrantLock 的子表。当我们去写数据时,程序会先定位到具体的 Segment,然后把这个段锁住。这样做的好处是,不同段之间的写操作可以并行,比整个 Map 加一把大锁(如 Hashtable)性能提升了很多。

但是,分段锁有两个比较明显的痛点:第一是并发度受限,默认只有 16,一旦初始化确定了,后期很难动态扩容;第二是内存浪费,每个 Segment 都是一个独立的锁对象,在数据量大时会占用较多额外内存。

到了 JDK 1.8,官方彻底废弃了分段锁,改为使用 CAS + synchronized。它的改进非常精妙:当向一个空的桶(Bucket)插入数据时,它直接利用 CAS 无锁操作尝试写入,完全没有加锁开销;只有当发生哈希冲突、需要操作链表或红黑树时,才会使用 synchronized 锁住该桶的头节点

这里其实涉及到一个高手经常会关注的细节:为什么 1.8 重新用回了 synchronized 而不是继续用 ReentrantLock?

主要有三点考量:第一是锁粒度更细了。分段锁是锁住一堆桶,而 1.8 是锁住单个桶的头节点。锁的粒度越细,冲突概率就越低。第二是性能优化。JDK 6 之后对 synchronized 做了大量底层优化,包括锁升级、自旋锁等,它的性能在现代 JVM 中已经非常优秀。第三是节省内存。如果用 ReentrantLock,我们需要给每个桶都创建一个锁对象,这太重了。而使用 synchronized 只需要利用对象头的 Mark Word 标记位,不占额外空间。

总结来说,ConcurrentHashMap 的演进就是从“粗粒度锁”向“细粒度锁”甚至“无锁化”发展的过程。

最后补充一个比较关键的细节:在 1.8 中,虽然读操作(get)大多数情况是不加锁的,这是因为 Node 的 valnext 指针都加了 volatile 关键字,保证了可见性,从而实现了真正的高性能并发读。

最后更新于:
⏱️

登录后开始阅读

当前 MVP 版本只开放邮箱密码登录注册。完成登录后即可进入面试题内容。

还没有账号?先去注册