为什么 ConcurrentHashMap 采用 synchronized 加锁而不采用ReentrantLock

文章目录

    • [1. 锁对象的演进:从"大锁"到"小锁"](#1. 锁对象的演进:从“大锁”到“小锁”)
    • [2. 为什么不全用原子操作(CAS)?](#2. 为什么不全用原子操作(CAS)?)
    • [3. 为什么选 `synchronized` 而不是 `ReentrantLock`?](#3. 为什么选 synchronized 而不是 ReentrantLock?)
      • [① 内置锁的"主场优势" (JVM 优化)](#① 内置锁的“主场优势” (JVM 优化))
      • [② 内存开销的极致压榨](#② 内存开销的极致压榨)
    • [4. 总结:现在的协作逻辑](#4. 总结:现在的协作逻辑)
    • 核心结论

这是一个快手的面试题,当时我被问到了
jdk1.7采用了segment+ReentranceLock
jdk1.8采用CAS+synchronized处理竞态
这是一个非常经典且能体现 Java 性能演进的问题。

其实 ConcurrentHashMap 并不是完全放弃了原子操作。准确地说,它现在的设计策略是:先用原子操作(CAS),万不得已才动用 synchronized

在 Java 8 之后,它的这种转变核心逻辑是为了在极细粒度 的锁竞争下,压榨出比 ReentrantLock 更高的性能。我们可以从以下几个维度来系统分析:


1. 锁对象的演进:从"大锁"到"小锁"

  • Java 7(Segment + ReentrantLock)
    它把 Map 分成了 16 个 Segment,每个 Segment 都是一个 ReentrantLock。这意味着如果你有两个不同的 Key 落在同一个 Segment 里,哪怕它们在不同的桶(Bucket)上,也得竞争同一把锁。
  • Java 8+(Node + synchronized)
    它取消了 Segment,直接锁住每个桶的第一个节点(Node)
    • CAS 负责"无中生有" :如果桶是空的,直接通过 CAS 插入,完全不加锁。
    • synchronized 负责"精细管理" :只有当桶里已经有数据(发生碰撞)时,才针对这一个桶的头节点加 synchronized

2. 为什么不全用原子操作(CAS)?

原子操作(如 compareAndSet)虽然快,但它只能保证单个变量修改的原子性。

  • 痛点:当发生哈希冲突时,我们需要在链表或红黑树上进行一系列复杂操作(比如遍历、比较、插入、平衡旋转)。
  • 局限性:要把这一整套"组合拳"全部封装进一个 CAS 里,逻辑会变得极其复杂且难以维护,甚至会因为频繁的自旋(Spinning)导致 CPU 空转,浪费大量资源。此时,使用锁来保护整个代码块是更稳妥、更高效的选择。

3. 为什么选 synchronized 而不是 ReentrantLock

这是很多人的误区,认为 synchronized 慢。但在 Java 8 及以后的版本中,情况发生了反转:

① 内置锁的"主场优势" (JVM 优化)

synchronized 是 JVM 的亲儿子,拥有各种"黑科技"加持:

  • 锁升级机制 :它会经历 偏向锁 -> 轻量级锁 -> 重量级锁 的动态演进。在竞争不激烈时,它的开销极低。
  • 锁消除与锁粗化 :JIT 编译器能根据代码运行情况,自动把没必要的锁删掉,或者把多个小锁合并。这些是作为第三方类库的 ReentrantLock 很难做到的。

② 内存开销的极致压榨

  • ReentrantLock 是一个对象,它需要维护 AQS 同步队列、锁状态等一系列成员变量。
  • 如果 Map 有 1024 个桶,全用 ReentrantLock 就要创建 1024 个锁对象,这非常浪费内存。
  • synchronized 直接利用对象头(Mark Word)里的位来标记锁状态,不需要额外创建锁对象 。对于 ConcurrentHashMap 这种动辄成千上万个桶的结构来说,内存节省非常可观。

最好看一下下面这三篇我写的文章,可以更好的理解
为什么不逃逸代表不需要锁,JIT会直接删掉锁
什么是逃逸分析
锁消除和锁粗化

4. 总结:现在的协作逻辑

ConcurrentHashMap 的源码里,一次 put 操作的逻辑是这样的:

  1. 计算哈希值
  2. 如果是空桶 :尝试用 CAS 插入。成功则返回。
  3. 如果正在扩容 :当前线程去协助搬迁(Help Transfer)。
  4. 如果是旧桶冲突 :使用 synchronized 锁住头节点,进行链表或红黑树的操作。

核心结论

ConcurrentHashMap 采用 synchronized 的核心痛点不是为了代替原子操作,而是为了在原子操作覆盖不到的复杂逻辑区(链表/树操作),提供一种内存更省、底层优化更强、且粒度更细的同步机制。

现在的 synchronized 已经不是当年那个笨重的"重量级锁"了,它在低竞争场景下的性能表现,已经足以让它在集合框架中重新坐稳"一哥"的位置。

相关推荐
阿丰资源2 小时前
java项目(附资料)-基于SpringBoot+MyBatisPlus+MySQL+Layui的药品管理系统
java·spring boot·mysql
云恒要逆袭2 小时前
Java SE、EE、ME到底啥区别?我被这个问题困扰了一整年
java·java ee
鱼鳞_2 小时前
Java学习笔记_Day27(Stream流)
java·笔记·学习
身如柳絮随风扬2 小时前
Servlet:访问流程、核心接口与生命周期
java·servlet·web
indexsunny2 小时前
互联网大厂Java面试实战:从Spring Boot到微服务架构的深度探讨
java·数据库·spring boot·安全·微服务·监控·面试实战
夕除2 小时前
javaweb--03
java
m0_677904842 小时前
K8s学习
java·学习·kubernetes
Fate_I_C3 小时前
Kotlin 内部类和嵌套类
java·开发语言·kotlin
宸津-代码粉碎机3 小时前
Spring Boot 4.0 实战技巧全解析
java·大数据·spring boot·后端·python