1. 整体架构与核心属性
JDK8 中的 ConcurrentHashMap
通过一系列精心设计的核心属性实现高效并发,这些属性控制着数据结构的初始化、扩容、并发控制及数据存储:
1.1. 数据存储核心:table 数组
transient volatile Node<K,V>[] table;
-
功能:
- 存储键值对的核心数组,初始长度为 16,扩容时长度翻倍
- 数组元素类型为
Node
,可能是普通链表节点、红黑树节点或扩容节点
-
关键点:
volatile
修饰确保可见性,一个线程对数组的修改对其他线程立即可见- 数组长度始终为 2 的幂,通过位运算替代取模,提升哈希效率
1.2. 扩容辅助:nextTable
private transient volatile Node<K,V>[] nextTable;
-
功能:
- 扩容时用于存储新数组,长度为原数组的 2 倍
- 扩容完成后,
nextTable
被置为null
,table
指向新数组
-
关键点:
- 扩容采用 多线程协作,通过
ForwardingNode
标记已迁移的桶 - 读操作遇到
ForwardingNode
时,会自动转发到nextTable
进行查询
- 扩容采用 多线程协作,通过
1.3. 全局控制:sizeCtl
private transient volatile int sizeCtl;
-
功能:
-
控制
table
的初始化和扩容操作,其值的不同状态代表不同含义:- -1:表示
table
正在初始化 - 负数且非 -1:表示正在扩容,
-N
表示有N-1
个线程正在参与扩容 - 0:默认值,表示
table
尚未初始化 - 正数:初始化阈值或下次扩容的阈值(初始为 12,即 16×0.75)
- -1:表示
-
-
关键点:
- 通过
CAS
操作修改sizeCtl
,确保多线程环境下的原子性 - 扩容时通过
resizeStamp(n) << RESIZE_STAMP_SHIFT + 2
生成特殊标记值
- 通过
1.4. 阈值控制:threshold
private final int threshold;
-
功能:
- 链表转红黑树的阈值:当链表长度达到 8 且数组长度≥64 时,转换为红黑树
- 红黑树转链表的阈值:当红黑树节点数减少到 6 时,转回链表
-
关键点:
- 与
sizeCtl
的区别:threshold
控制数据结构转换,sizeCtl
控制扩容
- 与
1.5. 修改计数:modCount
private transient volatile int modCount;
-
功能:
- 记录
ConcurrentHashMap
的结构修改次数(如插入、删除) - 用于支持弱一致性迭代器,确保迭代过程中不会抛出
ConcurrentModificationException
- 记录
-
关键点:
- 弱一致性保证:迭代器可能反映最新修改,也可能反映创建时的状态
1.6. 元素计数:baseCount 与 CounterCell
private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;
-
功能:
- baseCount:无竞争时,直接通过 CAS 更新该值统计元素数量
- CounterCell[] :竞争激烈时,各线程在不同的
CounterCell
中计数,最后合并结果
-
关键点:
- 类似
LongAdder
的分段计数思想,减少 CAS 冲突 - 元素总数 =
baseCount
+ 所有CounterCell
的值之和
- 类似
1.7. 哈希扰动:HASH_BITS
static final int HASH_BITS = 0x7fffffff;
-
功能:
- 用于计算哈希值时消除符号位影响(
(h ^ (h >>> 16)) & HASH_BITS
) - 确保哈希值为正数,因为负数哈希值有特殊含义(如
-1
表示ForwardingNode
)
- 用于计算哈希值时消除符号位影响(
归纳:
- table/nextTable:构建动态扩容的数据存储结构
- sizeCtl:统一管理初始化与扩容流程
- threshold:控制链表与红黑树的自适应转换
- baseCount/CounterCell:高效统计元素数量
- modCount:支持弱一致性迭代
1.8. 属性协作流程示例
1.8.1. 初始化流程
- 首次调用
put
时,检查table
是否为null
- 通过
CAS(sizeCtl, 0, -1)
抢占初始化权 - 创建长度为 16 的
table
数组 - 设置
sizeCtl
为扩容阈值(16×0.75 = 12)
1.8.2. 扩容触发与控制
- 元素数量超过
sizeCtl
时触发扩容 - 首个发起扩容的线程通过
CAS
将sizeCtl
设置为特殊负值(如- (resizeStamp(n) << RESIZE_STAMP_SHIFT + 2)
) - 创建
nextTable
(长度为原数组 2 倍) - 多线程协作迁移节点,每个线程负责一段桶的迁移
- 迁移完成后,
table
指向nextTable
,重置sizeCtl
为新阈值
1.8.3. 数据结构转换
- 插入元素时,若链表长度达到 8 且数组长度≥64,将链表转换为红黑树(
TreeBin
) - 删除元素时,若红黑树节点数减少到 6,将红黑树转回链表。
2. 红黑树与 TreeBin 的关系
2.1 TreeBin 与 Node 节点的关系
1. TreeBin 的本质:红黑树的容器包装类
TreeBin 是 JDK8 ConcurrentHashMap 为实现红黑树结构而设计的容器类,它继承自 Node 以确保能存储在 Node 数组中,但实际的键值对存储在其内部的 TreeNode 节点中。这种设计既保持了数据结构的统一性(所有桶元素都是 Node 子类),又通过封装实现了链表与红黑树的平滑转换
,是 JDK8 并发容器设计的重要优化点。
它的核心作用是将普通链表转换为红黑树时,对树结构进行封装。其类定义如下(JDK8 源码简化):
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; // 红黑树根节点
TreeNode<K,V> first; // 链表头节点(兼容遍历)
int size; // 树节点数量
// 其他并发控制相关属性...
}
2. TreeBin 与 Node 的继承关系
Node
是 ConcurrentHashMap 的基础节点抽象类TreeBin
继承自Node
,但它不直接存储键值对,而是存储红黑树的根节点- 真正的红黑树节点由
TreeNode
类实现,它继承自Node
:
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> left; // 左子节点
TreeNode<K,V> right; // 右子节点
TreeNode<K,V> parent; // 父节点
boolean red; // 红黑树颜色标记
// 其他树节点属性...
}
3. 数据结构中的角色定位
4. 关键区别对比表
类型 | 继承自 | 存储内容 | 核心作用 |
---|---|---|---|
NormalNode | Node | key/value/next | 构成普通链表节点 |
TreeNode | Node | key/value/left/right | 构成红黑树节点,包含树结构指针 |
TreeBin | Node | root(TreeNode 根节点) | 封装红黑树,提供树操作接口 |
5. 红黑树转换时的结构变化
当链表长度超过 8 时,会发生以下转换:
- 原链表的头节点被替换为
TreeBin
实例 TreeBin
的root
指向新创建的红黑树根节点- 原链表的所有
NormalNode
被转换为TreeNode
,构成红黑树结构 TreeBin
的first
指向原链表的头节点,保留链表遍历能力
6. 源码中的典型应用场景
// 当链表长度≥8时,转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, i);
// treeifyBin方法核心逻辑(简化)
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// 创建TreeBin并替换原链表头节点
tab[index] = new TreeBin<>(root);
}
}
3. 并发控制核心机制
3.1 锁粒度细化:从 Segment 到桶级锁
1. JDK8 的桶级锁架构
-
锁粒度:直接对数组中的 ** 单个桶(头节点)** 加锁
- 使用
synchronized
锁定桶的头节点(Node
或TreeBin
) - 锁范围从整个 Segment 缩小到单个哈希桶
- 使用
2. 锁优化带来的优势
- 并发度提升:理论并发度等于数组长度(默认 16 → 32 → 64...)
- 锁竞争减少:仅当多个线程同时访问同一桶时才会产生竞争
- 锁效率更高:
synchronized
经过 JVM 优化(偏向锁、轻量级锁),性能接近ReentrantLock
3. 源码关键实现
// putVal() 方法核心逻辑
synchronized (f) { // f 是桶的头节点
if (tabAt(tab, i) == f) { // 再次校验头节点未变
if (fh >= 0) { // 链表节点
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
// 遍历链表...
}
}
else if (f instanceof TreeBin) { // 红黑树节点
// 红黑树插入...
}
}
}
3.2 CAS+volatile 无锁化操作
1. volatile 保证可见性
-
关键字段:
val
和next
字段被声明为volatile
table
和nextTable
数组被声明为volatile
-
作用:
- 一个线程对节点的修改会立即刷新到主内存
- 其他线程对节点的读取会直接从主内存获取最新值
2. CAS 实现原子操作
-
典型应用场景:
- 初始化数组:使用
CAS(sizeCtl, 0, -1)
避免多线程重复初始化 - 插入头节点:使用
CAS(tab, i, null, new Node)
创建头节点 - 计数更新:使用
CAS(baseCount, expect, update)
或CounterCell
分段计数
- 初始化数组:使用
3. CAS 与锁的结合
- 无锁优先:在无竞争场景下(如插入头节点),优先使用 CAS 避免加锁
- 有锁兜底:在 CAS 失败或需要复杂操作时(如链表遍历),使用
synchronized
加锁
4. 源码关键实现
// 初始化数组时使用 CAS 避免冲突
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) // 有其他线程正在初始化
Thread.yield();
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // CAS 抢占初始化权
try {
if ((tab = table) == null || tab.length == 0) {
tab = (Node<K,V>[])new Node<?,?>[sc]; // 创建数组
table = tab;
sc = n - (n >>> 2); // 计算下次扩容阈值
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
3.3 并发控制的典型场景
1. 插入操作(put)
-
计算哈希值,定位桶位置
-
CAS 检查桶是否为空:为空则直接 CAS 插入新节点
-
加锁操作:桶不为空则对头节点加锁
- 链表:遍历并插入新节点
- 红黑树:调用树的插入方法
-
检查链表长度:超过阈值(8)则转换为红黑树
2. 扩容操作(transfer)
- 触发条件:元素数量超过
sizeCtl
- CAS 抢占扩容权:第一个发起扩容的线程通过
CAS(sizeCtl, sc, (sc < 0) ? sc - 1 : resizeStamp(n) << RESIZE_STAMP_SHIFT + 2)
获取扩容资格 - 分段迁移:将原数组分为多个段,多线程各自负责一段的迁移
- 标记已迁移桶:迁移完成的桶用
ForwardingNode
替换原头节点,ForwardingNode
的nextTable
指向新数组
3. 读取操作(get)
-
计算哈希值,定位桶位置
-
检查头节点类型:
- 普通节点 / 树节点:直接读取
ForwardingNode
:通过nextTable
到新数组读取
-
全程无锁:由于
val
和next
是volatile
,保证读取的可见性
4、性能对比与优势
特性 | JDK7 ConcurrentHashMap | JDK8 ConcurrentHashMap |
---|---|---|
锁粒度 | Segment(默认 16 个) | 单个桶(数组长度) |
锁实现 | ReentrantLock | synchronized + CAS |
并发度 | 固定为 Segment 数量(默认 16) | 随数组扩容动态增加 |
哈希冲突处理 | 仅链表 | 链表 + 红黑树(长度 > 8 时转换) |
读操作 | 无锁,但需遍历 Segment | 无锁,直接定位桶 |
写操作 | 锁定整个 Segment | 仅锁定单个桶 |
这种设计使 JDK8 ConcurrentHashMap 在高并发场景下的吞吐量显著提升,例如:
- 读操作:几乎无锁竞争,性能接近无锁的
HashMap
- 写操作:锁粒度细化后,并发写入的冲突概率大幅降低
3. 核心设计总结
-
数据结构三层形态:
- 数组:基础存储容器,初始 16 长度
- 链表:哈希冲突少时使用,长度≤8
- 红黑树:链表长度 > 8 时转换,查询 O (logN)
-
并发控制优化:
- 锁粒度细化:仅锁定操作桶的头节点
- CAS+volatile:减少锁竞争,保证可见性
-
扩容机制:
- ForwardingNode 标记已迁移桶
- 多线程协作迁移,读操作自动转发到新数组
- 渐进式扩容,无全局停顿
-
性能关键点:
- 红黑树解决哈希冲突极端情况
- 高位哈希扰动 (hash ^ (hash>>>16)) 提升分布均匀性
- 懒初始化避免资源浪费