回答思路
- 版本区分:明确说明JDK 1.7和1.8的实现有本质不同。
- JDK 1.7:分段锁机制
- JDK 1.8:CAS + synchronized 精细化锁
- 总结对比
详细回答
ConcurrentHashMap 是线程安全的HashMap,它的实现方式在JDK 1.7和1.8中有根本性的变革,但目标都是减少锁的粒度,提升并发性能。
一、JDK 1.7 的实现:分段锁(Segment Locking)
在JDK 1.7中,ConcurrentHashMap 采用了 "分段锁" 的策略,这是一种典型的"锁分离"技术。
核心数据结构:
- 它内部维护了一个 Segment 数组 ,每个 Segment 本质上是一个独立的、继承自
ReentrantLock的哈希表。 - 每个 Segment 内部又维护了一个 HashEntry 数组,也就是真正存储键值对的地方。
工作原理:
- 分段 :默认有 16 个 Segment(并发级别)。可以理解为有16个独立的"小HashMap",每个都有自己的锁。
- 哈希定位 :
- 插入或读取一个元素时,首先对key进行哈希计算,先用一次哈希决定数据属于哪个 Segment。
- 再用一次哈希决定数据在这个 Segment 内的 HashEntry 数组中的位置。
- 加锁 :
- 当多个线程访问不同的 Segment 时,它们可以完全并行,因为锁是不同的。
- 只有当多个线程访问同一个 Segment 时,才会发生竞争,需要获取同一个锁。
优点:
- 相比
Hashtable那种对整个数组加synchronized的方式,并发度大大提升。默认16个分段,理论上最多支持16个线程并发写入。
缺点:
- 数据结构复杂,查询需要两次哈希。
- 锁的粒度虽然是Segment,但依然不够细。如果一个Segment内部存储了大量数据,竞争依然会激烈。
二、JDK 1.8 的实现:CAS + synchronized
JDK 1.8 抛弃了分段锁的设计,采用了与 HashMap 更相似的 数组 + 链表 / 红黑树 结构,并利用 CAS(Compare-And-Swap) 和 对链表头/树根节点的 synchronized 来实现更细粒度的线程安全。
核心思想:将锁的粒度从"一段"缩小到"一个桶(链表头节点/树根节点)"。
具体实现方式:
-
使用
Node数组作为核心桶数组 :结构几乎与HashMap一样。 -
put操作流程(核心看如何保证线程安全):- 步骤一:检查桶是否为空(初始化或扩容) 。这里使用 CAS 来保证只有一个线程能初始化数组或触发扩容。
- 步骤二:定位到具体的桶(数组下标) 。
- 如果该桶为
null,则使用 CAS 操作将新节点插入到该位置。(无锁操作) - 为什么用CAS? 因为如果桶为空,说明没有竞争,用轻量级的CAS比直接加锁性能更高。
- 如果该桶为
- 步骤三:如果该桶不为
null(说明发生了哈希碰撞)。- 对当前桶的头节点(或红黑树的根节点)加
synchronized锁。 - 然后在同步块内进行链表遍历插入或红黑树插入。
- 为什么用
synchronized而不是ReentrantLock? 因为JVM对synchronized做了大量优化(如锁升级),在低竞争场景下性能很好,且可以减少内存开销。
- 对当前桶的头节点(或红黑树的根节点)加
-
get操作流程:- 完全无锁 。因为
Node的val和next属性都用volatile修饰。 volatile保证了线程的可见性,一个线程的修改能立刻被其他线程看到。- 通过
volatile读,可以安全地遍历链表或树,获取最新值。
- 完全无锁 。因为
-
扩容机制:
- JDK 1.8的扩容更高效,支持多线程协同扩容。
- 当某个线程插入数据时发现需要扩容,它会开始转移自己负责的桶,并帮助转移其他桶。
- 扩容期间,
get操作仍然可以无锁进行(可能需要在旧表和新表中查找)。put操作如果遇到正在转移的桶,也会帮忙一起转移。
三、总结对比与核心要点
| 特性 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 锁机制 | 分段锁(ReentrantLock) |
CAS + synchronized |
| 锁粒度 | Segment(一批桶) | 单个桶的头节点(更细) |
| 数据结构 | Segment数组 + HashEntry数组 + 链表 | Node数组 + 链表/红黑树 |
get 操作 |
需要两次哈希,使用 volatile 保证可见性 |
一次哈希,完全无锁,性能更高 |
| 并发度 | 受限于Segment数量 | 理论上是桶的数量,更高 |
面试回答核心要点:
- JDK 1.8 是现在的标准,重点阐述它的实现。
- 它的线程安全建立在三块基石上:
- CAS:用于无竞争情况下的原子更新(如初始化、空桶插入),性能极高。
synchronized:用于有哈希碰撞时,锁住单个桶的头节点,锁粒度非常细。volatile:用于保证get操作的可见性,实现无锁读。
- 这种设计使得在低冲突 的情况下,性能接近
HashMap,而在高并发环境下又能提供出色的线程安全和高吞吐量。