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

相关推荐
sheji34166 分钟前
【开题答辩全过程】以 基于springboot的房屋租赁系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
木井巳12 分钟前
【递归算法】子集
java·算法·leetcode·决策树·深度优先
行百里er1 小时前
优雅应对异常,从“try-catch堆砌”到“设计驱动”
java·后端·代码规范
娇娇yyyyyy1 小时前
QT编程(17): Qt 实现自定义列表模型
开发语言·qt
ms_27_data_develop1 小时前
Java枚举类、异常、常用类
java·开发语言
xiaohe071 小时前
Spring Boot 各种事务操作实战(自动回滚、手动回滚、部分回滚)
java·数据库·spring boot
代码飞天2 小时前
wireshark的高级使用
android·java·wireshark
add45a2 小时前
C++编译期数据结构
开发语言·c++·算法
gechunlian882 小时前
Spring Boot中的404错误:原因、影响及处理策略
java·spring boot·后端