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,而在高并发环境下又能提供出色的线程安全和高吞吐量。

相关推荐
SamDeepThinking4 分钟前
我们当年是如何真实落地BFF的?
java·后端·架构
码语智行6 分钟前
Shapefile获取空间数据和中心点坐标
java·arcgis
caoyc6 分钟前
RAG 赛道全景扫描:ragflow 一骑绝尘、微软谷歌跟进乏力、下半场属于 Agent
java
阿正的梦工坊12 分钟前
【Rust】09-泛型、Trait 与生命周期基础
开发语言·rust·c#
屋外雨大,惊蛰出没19 分钟前
深入浅出Spring Boot
java·spring boot·ioc·aop
阿正的梦工坊35 分钟前
【Rust】07-错误处理:Option、Result 与 ? 运算符
开发语言·算法·rust
Zella折耳根39 分钟前
复习篇-继承和接口
java·开发语言·python
z落落42 分钟前
C# 事件(Event)+自定义带参数事件例子
开发语言·分布式·c#
FlYFlOWERANDLEAF43 分钟前
DevExpress Office File API使用记录
开发语言·c#·devoffice