ConcurrentHashMap 的线程安全实现

回答思路

  1. 版本区分:明确说明JDK 1.7和1.8的实现有本质不同。
  2. JDK 1.7:分段锁机制
  3. JDK 1.8:CAS + synchronized 精细化锁
  4. 总结对比

详细回答

ConcurrentHashMap 是线程安全的HashMap,它的实现方式在JDK 1.7和1.8中有根本性的变革,但目标都是减少锁的粒度,提升并发性能。

一、JDK 1.7 的实现:分段锁(Segment Locking)

在JDK 1.7中,ConcurrentHashMap 采用了 "分段锁" 的策略,这是一种典型的"锁分离"技术。

核心数据结构:

  • 它内部维护了一个 Segment 数组 ,每个 Segment 本质上是一个独立的、继承自 ReentrantLock 的哈希表。
  • 每个 Segment 内部又维护了一个 HashEntry 数组,也就是真正存储键值对的地方。

工作原理:

  1. 分段 :默认有 16 个 Segment(并发级别)。可以理解为有16个独立的"小HashMap",每个都有自己的锁。
  2. 哈希定位
    • 插入或读取一个元素时,首先对key进行哈希计算,先用一次哈希决定数据属于哪个 Segment
    • 再用一次哈希决定数据在这个 Segment 内的 HashEntry 数组中的位置
  3. 加锁
    • 当多个线程访问不同的 Segment 时,它们可以完全并行,因为锁是不同的。
    • 只有当多个线程访问同一个 Segment 时,才会发生竞争,需要获取同一个锁。

优点:

  • 相比 Hashtable 那种对整个数组加synchronized的方式,并发度大大提升。默认16个分段,理论上最多支持16个线程并发写入。

缺点:

  • 数据结构复杂,查询需要两次哈希。
  • 锁的粒度虽然是Segment,但依然不够细。如果一个Segment内部存储了大量数据,竞争依然会激烈。
二、JDK 1.8 的实现:CAS + synchronized

JDK 1.8 抛弃了分段锁的设计,采用了与 HashMap 更相似的 数组 + 链表 / 红黑树 结构,并利用 CAS(Compare-And-Swap)对链表头/树根节点的 synchronized 来实现更细粒度的线程安全。

核心思想:将锁的粒度从"一段"缩小到"一个桶(链表头节点/树根节点)"。

具体实现方式:

  1. 使用 Node 数组作为核心桶数组 :结构几乎与 HashMap 一样。

  2. put 操作流程(核心看如何保证线程安全)

    • 步骤一:检查桶是否为空(初始化或扩容) 。这里使用 CAS 来保证只有一个线程能初始化数组或触发扩容。
    • 步骤二:定位到具体的桶(数组下标)
      • 如果该桶为 null,则使用 CAS 操作将新节点插入到该位置。(无锁操作)
      • 为什么用CAS? 因为如果桶为空,说明没有竞争,用轻量级的CAS比直接加锁性能更高。
    • 步骤三:如果该桶不为 null (说明发生了哈希碰撞)。
      • 对当前桶的头节点(或红黑树的根节点)加 synchronized
      • 然后在同步块内进行链表遍历插入或红黑树插入。
      • 为什么用 synchronized 而不是 ReentrantLock 因为JVM对synchronized做了大量优化(如锁升级),在低竞争场景下性能很好,且可以减少内存开销。
  3. get 操作流程

    • 完全无锁 。因为 Nodevalnext 属性都用 volatile 修饰。
    • volatile 保证了线程的可见性,一个线程的修改能立刻被其他线程看到。
    • 通过 volatile 读,可以安全地遍历链表或树,获取最新值。
  4. 扩容机制

    • JDK 1.8的扩容更高效,支持多线程协同扩容
    • 当某个线程插入数据时发现需要扩容,它会开始转移自己负责的桶,并帮助转移其他桶。
    • 扩容期间,get 操作仍然可以无锁进行(可能需要在旧表和新表中查找)。put 操作如果遇到正在转移的桶,也会帮忙一起转移。
三、总结对比与核心要点
特性 JDK 1.7 JDK 1.8
锁机制 分段锁(ReentrantLock CAS + synchronized
锁粒度 Segment(一批桶) 单个桶的头节点(更细)
数据结构 Segment数组 + HashEntry数组 + 链表 Node数组 + 链表/红黑树
get 操作 需要两次哈希,使用 volatile 保证可见性 一次哈希,完全无锁,性能更高
并发度 受限于Segment数量 理论上是桶的数量,更高

面试回答核心要点:

  1. JDK 1.8 是现在的标准,重点阐述它的实现。
  2. 它的线程安全建立在三块基石上:
    • CAS:用于无竞争情况下的原子更新(如初始化、空桶插入),性能极高。
    • synchronized:用于有哈希碰撞时,锁住单个桶的头节点,锁粒度非常细。
    • volatile :用于保证 get 操作的可见性,实现无锁读。
  3. 这种设计使得在低冲突 的情况下,性能接近 HashMap,而在高并发环境下又能提供出色的线程安全和高吞吐量。

相关推荐
Scott9999HH7 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔8 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社8 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海9 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖9 小时前
JDK 26 新特性详解
java·开发语言
马优晨10 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区12 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大12 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai13 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说13 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json