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

相关推荐
fenglllle7 小时前
JDK8升级JDK17使用CompletableFuture在线程中classloader的变化
java·开发语言·jvm
计算机安禾7 小时前
【c++面向对象编程】第44篇:typename与class的区别,依赖类型名与template消除歧义
java·jvm·c++
JAVA面经实录9177 小时前
Java+SpringAI企业级实战项目完整官方文档(生产终版)
java·开发语言·spring·ai编程
梵得儿SHI7 小时前
Java IO 流进阶:Buffer 与 Channel 核心概念解析及与传统 IO 的本质区别
java·开发语言·高并发·nio·channel·buffer·提升io效率
2301_789015627 小时前
C++_string增删查改模拟实现
java·开发语言·c++
没有逆称7 小时前
Java OOM 问题全解析
java·jvm
星河耀银海7 小时前
JAVA 注解(Annotation):从原理到实战应用
java·开发语言·数据库
AI人工智能+电脑小能手7 小时前
【大白话说Java面试题 第68题】【JVM篇】第28题:对于 JDK 自带的监控和性能分析工具用过哪些?一般你怎么用的?
java·开发语言·jvm·面试
青梅橘子皮8 小时前
Linux---冯诺伊曼体系结构,操作系统概况
java·linux·运维
Hexian25808 小时前
SpringAI MCP
java·spring·ai