为什么 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 已经不是当年那个笨重的"重量级锁"了,它在低竞争场景下的性能表现,已经足以让它在集合框架中重新坐稳"一哥"的位置。

相关推荐
_Evan_Yao5 小时前
Redis 缓存穿透、击穿、雪崩,一次讲清!
java·spring·mybatis
梓豪csd5 小时前
消息订阅中心:谁该收到哪条通知
java·开发语言·人工智能·低代码·自动化·1024程序员节
秋饼5 小时前
Spring AI 2.1 Message Parts 深度实战:推理签名回放与跨轮记忆持久化的源码级落地
java·ai·技术分享·后端开发
thefool1122666 小时前
TCP 1
java·1024程序员节
金銀銅鐵8 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(下)
java·后端·ai编程
AI人工智能+电脑小能手9 小时前
一次请求打几个下游,这个Java并行任务工具类6个方法按序聚合
java·线程池·并发编程·1024程序员节·批量处理·并行任务
paopaokaka_luck10 小时前
心理咨询管理系统(WebSocket实时沟通、基于用户的协同过滤、 ECharts多维分析、 预约业务流程、预约校验与定时处理)
java·vue.js·spring boot·mysql·spring·echarts·1024程序员节
2301_322414280411 小时前
springboot农产品交易平台的设计与实现08055-计算机毕设 原创(免费领源码+部署教程+开发环境)
java·spring boot·redis·mysql·tomcat·maven·mybatis
考虑考虑11 小时前
JDK26的JEP 500属性
java·后端·java ee
LMT流氓兔12 小时前
第五单元——第十二课:把脚本整理成项目
java·数据库·python·langchain·1024程序员节