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

相关推荐
听风吟丶2 小时前
Java 9+ 模块化系统(Jigsaw)实战:从 Jar 地狱到模块解耦的架构升级
java·架构·jar
昂子的博客2 小时前
Redis缓存 更新策略 双写一致 缓存穿透 击穿 雪崩 解决方案... 一篇文章带你学透
java·数据库·redis·后端·spring·缓存
Dxy12393102162 小时前
Python为什么要使用可迭代对象
开发语言·python
百***68822 小时前
SpringBoot中Get请求和POST请求接收参数详解
java·spring boot·spring
百***41662 小时前
Java MySQL 连接
java·mysql·adb
Jayden2 小时前
synchronized全解析:从锁升级到性能优化,彻底掌握Java内置锁
java·synchronized·synchronized面试·synchronized扫盲
任子菲阳3 小时前
学Java第四十五天——斗地主小游戏创作
java·开发语言·windows
czhc11400756633 小时前
Java1112 基类 c#vscode使用 程序结构
android·java·数据库
嫂子的姐夫3 小时前
23-MD5+DES+Webpack:考试宝
java·爬虫·python·webpack·node.js·逆向