1、什么是ConcurrentHashMap
ConcurrentHashMap是java的并发容器,线程安全的HashMap,解决原生HashMap并发死链、数据丢失,以及解决HashTable全局锁性能低下的问题,适用于多线程并发读写场景。
2、为什么需要ConcurrentHashMap
2.1 HashMap 的线程安全问题
HashMap 是非线程安全的。多线程环境下,同时进行 put 操作可能导致死循环(JDK 1.7 头插法)、数据丢失或脏读。即便在 JDK 1.8 中改为尾插法,resize 期间仍可能出现数据不一致。
2.2 Hashtable 的局限性
Hashtable 虽然线程安全,但所有方法都用 synchronized 修饰,相当于给整个对象加锁。高并发下所有操作串行执行,性能急剧下降,无法发挥多核 CPU 的优势。
2.3 ConcurrentHashMap 的登场
ConcurrentHashMap 自 JDK 1.5 引入,经过 JDK 1.8 的重构,采用 CAS + synchronized 的细粒度锁机制,在保证线程安全的同时,将锁竞争降到最低,成为并发场景下 Map 的首选实现。
3、底层数据结构(JDK1.8+)
3.1、数据结构
ConcurrentHashMap的数据结构采用的数组+链表/红黑树。采用数组可以实现随机访问对应的数组下标数据,为了解决哈希冲突问题,使用了链表法,为了防止链表长度过长导致插入/查询性能下降进而增加了红黑树的存储方式。
- 数组:Node<K, V> table,哈希桶数组,延迟初始化(首次插入时创建)
- 链表:当哈希较少冲突时,采用单向链表存储节点
- 红黑树:当链表长度超过阈值默认8且数组长度>=64时,链表转化为红黑树,提高检索效率

链表上的数字都是经过计算的:
java
public class MapExample {
public static void main(String[] args) {
System.out.println(5 & 15); // 5
System.out.println(21 & 15); // 5
System.out.println(37 & 15); // 5
System.out.println(53 & 15); // 5
System.out.println(69 & 15); // 5
System.out.println(85 & 15); // 5
System.out.println(8 & 15); // 8
System.out.println(24 & 15); // 8
System.out.println(40 & 15); // 8
System.out.println(56 & 15); // 8
System.out.println(72 & 15); // 8
System.out.println(88 & 15); // 8
System.out.println(104 & 15); // 8
System.out.println(120 & 15); // 8
System.out.println(136 & 15); // 8
}
}
3.2、核心成员变量
java
transient volatile Node<K,V>[] table; // 哈希桶数组
private transient volatile Node<K,V>[] nextTable; // 扩容时的新数组
private transient volatile long baseCount; // 基础计数器
private transient volatile int sizeCtl; // 控制标识
3.3、sizeCtl状态(重要)
sizeCtl=0,tab数组还未创建
sizeCtl>0, 数组初始化完成, 扩容阈值16*0.75=12
sizeCtl=-1, 正在初始化tab数组
sizeCtl<0 不等于-1, 正在扩容,
高16位:`resizeStamp(tab.length)`,本轮扩容的标记
低16位: n+1, n=当前帮助扩容的线程数
有线程加入扩容+1, 线程退出扩容-1,sizeCtl恢复为正数,代替扩容结束,新的扩容阈值

4、源码分析
4.1、put
- tab如果未初始化,则先初始化
- 计算key的索引下标
- 第一:对应的数组为空则直接放入
- 第二:当前桶处于已迁移状态,参与扩容(符合扩容条件),高低位扩容
- 第三:不为空,在当前槽位增加synchronized加锁
- 如果是链表则在尾部加入节点,是否触发转化红黑树
- 如果是红黑树也将节点加入)
4.1.1、putVal(主方法)
java
public V put(K key, V value) {
return putVal(key, value, false);
}
指定槽位加锁synchronized保证安全,锁粒度小,并发性能好
java
/**
* ConcurrentHashMap 的 put 核心方法实现
*
* @param key 键
* @param value 值
* @param onlyIfAbsent 如果为 true,只有在该 key 不存在时才插入(即 putIfAbsent 逻辑)
* @return 被覆盖的旧值,如果没有旧值则返回 null
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 1. 前置校验:ConcurrentHashMap 不允许 key 或 value 为 null(区别于 HashMap)
if (key == null || value == null) throw new NullPointerException();
// 2. 对 key.hashCode() 进行扰动计算(高低位异或),减少哈希冲突
int hash = spread(key.hashCode());
// 3. binCount 用来记录桶(bin)中的元素个数(链表长度)
// 用于后续判断是否需要将链表转换为红黑树
int binCount = 0;
// 4. 自旋(死循环),直到插入操作成功为止
// 这是典型的 CAS 乐观锁重试机制
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 5. 如果哈希表还未初始化(延迟加载),则调用 initTable 进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 6. 根据哈希值定位桶位 (n - 1) & hash,并取出该桶的首节点 f
// 如果桶为空(f == null),说明没有发生哈希冲突
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 7. 桶为空时,创建新节点,通过 CAS(原子比较并交换)尝试放入该桶
// 如果 CAS 成功,break 跳出循环;如果失败,说明有其他线程抢先插入了,进入下一轮自旋重试
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // 这里没有加锁,是性能最高的无锁操作
}
// 8. 如果桶的首节点 f 的 hash 值为 MOVED(即 -1)
// 说明当前 table 正在扩容,且该桶已经被迁移(节点类型为 ForwardingNode)
else if ((fh = f.hash) == MOVED)
// 当前线程不阻塞,反而主动帮助(helpTransfer)扩容,加快迁移速度
// 扩容完成后,自旋继续执行插入
tab = helpTransfer(tab, f);
// 9. 桶不为空,且没有处于扩容状态(即正常写冲突情况)
else {
V oldVal = null;
// 10. 细粒度锁:锁定该桶的首节点 f(而不是锁定整个 table)
// 不同的桶可以同时被不同线程修改,互不影响
synchronized (f) {
// 11. 双重检查(Double-check):加锁成功后,再次确认首节点没有被其他线程改变
if (tabAt(tab, i) == f) {
// 12. 分支一:如果 fh >= 0,说明 f 是一个普通链表节点(非树节点,也非转发节点)
if (fh >= 0) {
binCount = 1; // 链表长度计数初始为 1
// 13. 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 14. 查找当前 key 是否存在(hash 相等,且 key 对象相等或 equals 相等)
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val; // 记录旧值
// 15. 如果 onlyIfAbsent 为 false(普通 put),则覆盖旧值
// 如果为 true(putIfAbsent),则保留旧值,不覆盖
if (!onlyIfAbsent)
e.val = value;
break; // 找到并操作完毕,跳出遍历
}
// 16. 未找到匹配的 key,继续遍历下一个节点
Node<K,V> pred = e;
if ((e = e.next) == null) {
// 17. 遍历到链表尾部,说明 key 不存在,直接尾插法创建新节点
pred.next = new Node<K,V>(hash, key,
value, null);
break; // 插入完毕,跳出遍历
}
}
}
// 18. 分支二:如果 f 是 TreeBin 类型(红黑树的根节点包装类)
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2; // 树结构只需设为 2(大于 0 即可,且不会触发转树逻辑)
// 19. 调用红黑树的插入方法 putTreeVal,如果返回 null 表示成功插入新节点
// 如果返回非 null,表示返回的是已存在的节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val; // 记录旧值
// 20. 同样根据 onlyIfAbsent 决定是否覆盖旧值
if (!onlyIfAbsent)
p.val = value;
}
}
}
} // synchronized 锁释放(锁的是首节点 f)
// 21. 后续处理(只要 binCount 不等于 0,说明上面的插入/更新操作已执行)
if (binCount != 0) {
// 22. 如果链表长度(binCount)大于等于树化阈值(默认 8)
// 尝试将链表转换为红黑树(注意:treeifyBin 内部还会判断数组长度是否 >= 64)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
// 23. 如果 oldVal 不为 null,说明是更新(覆盖)操作,直接返回旧值
if (oldVal != null)
return oldVal;
break; // 如果是插入新节点(无旧值),跳出主循环
}
}
} // 主循环结束(CAS 插入成功 或 synchronized 插入/更新完毕)
// 24. addCount:更新 Map 中的元素总数(baseCount + CounterCell)
// 内部会检查是否需要扩容,如果需要则触发扩容或协助扩容
addCount(1L, binCount);
return null; // 插入新节点时返回 null
}
4.1.2、协助扩容
java
/**
* 协助数据迁移(扩容)。
* 当线程在 put 或 compute 等操作中发现某个桶的节点类型为 ForwardingNode(hash == MOVED)时,
* 意味着当前 table 正在扩容,该线程不会阻塞等待,而是调用此方法主动参与迁移,
* 充分利用多核 CPU 资源,加速扩容过程。
*
* @param tab 当前主哈希表数组(扩容前的旧 table)
* @param f 当前桶中的首节点,类型必须为 ForwardingNode(转发节点)
* @return 扩容后的新哈希表数组(nextTable),方便调用方继续后续操作
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
// 1. 声明局部变量:nextTab 指向新表,sc 为 sizeCtl 控制值
Node<K,V>[] nextTab; int sc;
// 2. 多重校验:确保旧表不为空,且当前桶首节点确实是 ForwardingNode 类型,
// 并且能从该转发节点中获取到新表(nextTable)的引用
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 3. 计算扩容批次戳(resizeStamp)并左移 RESIZE_STAMP_SHIFT 位
// resizeStamp(tab.length) 会生成一个与当前数组容量(n)相关的唯一标识戳,
// 左移 16 位(RESIZE_STAMP_SHIFT = 16)后,用于在 sizeCtl 的高位存储扩容标记,
// 这样 sizeCtl 的负值(高位为 1)既表示正在扩容,又包含了扩容的唯一批次信息。
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
// 4. 自旋(循环判断),用于 CAS 竞争加入扩容任务
// 条件必须同时满足:
// - nextTab == nextTable:确保新表没有被其他线程替换
// - table == tab:确保旧表没有被重新初始化
// - (sc = sizeCtl) < 0:确保扩容仍在进行中(sizeCtl 为负表示扩容中)
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 5. 判断是否不需要当前线程参与扩容,满足以下任一条件则直接 break 退出循环:
// a) sc == rs + MAX_RESIZERS:表示参与扩容的线程数已达上限(MAX_RESIZERS - 1)
// b) sc == rs + 1:表示扩容任务已接近尾声(只剩最后一个辅助线程,即发起扩容的线程),
// 或者表示没有待迁移的桶了(transferIndex <= 0 会触发此判断)
// c) transferIndex <= 0:表示所有待迁移的桶都已经被分配完毕
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
transferIndex <= 0)
break;
// 6. 尝试使用 CAS 将 sizeCtl 的值增加 1(即 sc + 1)
// 这个操作表示当前线程申请加入扩容工作,使活跃的扩容线程数 +1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
// 7. CAS 成功后,调用 transfer(tab, nextTab) 执行实质性的数据迁移工作
// transfer 方法会将旧 table 中的节点重新哈希到新 table 中,
// 并且每个线程负责迁移一块连续范围的桶(通过 transferIndex 划分任务)
transfer(tab, nextTab);
break; // 迁移完成(或当前线程负责的部分完成),跳出循环
}
// 8. 如果 CAS 失败,说明 sizeCtl 被其他线程抢先修改了,
// 则继续 while 循环重新读取 sc,再次尝试竞争加入扩容
}
// 9. 返回新表引用(nextTab),调用方拿到新表后可以继续执行插入等操作
return nextTab;
}
// 10. 如果前置校验不通过(例如当前没有扩容,或节点不是 ForwardingNode),
// 则直接返回旧表(table),调用方会按照非扩容逻辑继续处理
return table;
}
触发真实的扩容逻辑:
1、先初始化新表 nextTab
2、一个迁移的线程负责一段数组区间,互不干扰
使用transferIndex默认为数组长度,一个线程负责的区间stride(最小为16);
transferIndex<=0则不能在增加线程来参与扩容了
举例比如:数组长度为64,区间为16
线程一负责区间:48 64
线程二负责区间:32 48
线程三负责区间:16 32
线程四负责区间:0 16
3、迁移
如果桶为空,直接设置成ForwardingNode
如果桶已经被迁移走,跳过
如果桶非空,桶首节点增加synchronized锁,迁移链表/红黑树
java
/**
* 将旧表(tab)中每个桶的节点迁移(或复制)到新表(nextTab)。
* 这是扩容的核心工作线程(可能多个并发执行)所执行的方法。
*
* @param tab 旧表数组
* @param nextTab 新表数组(可能为 null,若为 null 则由当前线程负责初始化)
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 1. 计算每个线程负责的桶区间步长(stride)
int n = tab.length, stride;
// 如果 CPU 核心数 > 1,步长 = n / 8 / NCPU,否则步长 = n
// 保证最小步长为 MIN_TRANSFER_STRIDE(默认 16),避免过度细碎
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // 最小为 16
// 2. 初始化新表(如果还未创建)
if (nextTab == null) { // initiating ------ 当前线程是首个发起扩容的线程
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 容量翻倍
nextTab = nt;
} catch (Throwable ex) { // 如果 OOM,则将 sizeCtl 设为最大,不再尝试扩容
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab; // 将新表赋给成员变量 nextTable(volatile 可见)
transferIndex = n; // 全局迁移索引初始为 n(表示旧表长度,从后往前分配任务)
}
int nextn = nextTab.length; // 新表长度
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // 预备转发节点(指向新表)
boolean advance = true; // 表示当前线程能否继续向前推进(分配下一个待迁移桶)
boolean finishing = false; // 是否所有迁移工作都已结束,用于提交前二次检查
// 3. 主循环:遍历旧表所有桶,逐个迁移
// i 表示当前处理的桶索引,bound 表示当前线程负责区间的下界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 4. 分配迁移任务(决定下一个要处理的桶索引 i)
while (advance) {
int nextIndex, nextBound;
// 如果当前桶索引 i 还未到达下界 bound,或者已经完成全部迁移(finishing),则无需再分配
if (--i >= bound || finishing)
advance = false;
// 否则,尝试从全局 transferIndex 中获取一个新区间
else if ((nextIndex = transferIndex) <= 0) {
// 如果 transferIndex <= 0,说明所有桶都已被分配完,设置 i = -1 表示无任务
i = -1;
advance = false;
}
// CAS 抢占下一个区间 [nextBound, nextIndex)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound; // 本线程负责的下界
i = nextIndex - 1; // 本线程开始处理的桶索引(从高到低)
advance = false; // 任务分配完毕,退出 while 循环,开始迁移
}
}
// 5. 检查是否所有桶都已处理完毕(i 越界)
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 如果 finishing 为 true,说明所有线程都已经完成了迁移,可以进行最终提交
if (finishing) {
nextTable = null; // 清除临时引用
table = nextTab; // 新表替换旧表
sizeCtl = (n << 1) - (n >>> 1); // 设置新阈值:容量 * 0.75(n << 1 是新容量)
return; // 扩容完成,返回
}
// 否则当前线程完成了自己的任务,尝试将 sizeCtl 减 1(表示自己退出扩容)
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 如果 (sc - 2) 不等于扩容戳左移的值,说明自己不是最后一个退出的线程,直接返回
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 如果自己是最后一个退出的线程,设置 finishing = advance = true,并令 i = n(重新检查一遍)
finishing = advance = true;
i = n; // 重新扫描旧表,确保所有桶都已标记为 ForwardingNode
}
}
// 6. 处理当前桶 i(如果还未被迁移)
else if ((f = tabAt(tab, i)) == null)
// 如果桶为空,直接 CAS 放入 ForwardingNode 节点,标记为已迁移
advance = casTabAt(tab, i, null, fwd);
// 如果桶首节点 hash == MOVED,说明已经被其他线程迁移过了,跳过
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// 7. 桶非空且未被迁移,开始实际迁移(加锁,只锁当前桶首节点)
else {
synchronized (f) {
// 双重检查,确保首节点未被其他线程修改
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn; // 低位链表(留在原位)和高位链表(搬到 i+n)
// -------------------- 处理链表 --------------------
if (fh >= 0) {
// 1) 先遍历一次,找到最后一个相邻的、哈希高低位相同的连续节点 lastRun
int runBit = fh & n; // 根据旧表长度 n 计算高位标志(0 或 n)
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 2) 根据 runBit 分类 lastRun 和后续节点
if (runBit == 0) {
ln = lastRun;
hn = null;
} else {
hn = lastRun;
ln = null;
}
// 3) 从首节点到 lastRun 之前的节点,重新创建节点并按高低位放入两个链表
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 4) 将 ln 放到新表原索引 i,hn 放到新表 i+n
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 5) 旧表对应位置标记为 ForwardingNode
setTabAt(tab, i, fwd);
advance = true; // 本桶迁移完成,可以推进下一个
}
// -------------------- 处理红黑树 --------------------
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null; // 低位树
TreeNode<K,V> hi = null, hiTail = null; // 高位树
int lc = 0, hc = 0;
// 遍历树的所有节点,按 (hash & n) 分为两组
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
} else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 如果拆分后的树节点数 <= UNTREEIFY_THRESHOLD(6),则退化为链表
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
// 放入新表
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 旧表标记为已迁移
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
4.2、get
-
首节点命中:比较hash和key,直接返回value
-
特殊节点:(hash<0)
-
-1 (ForwardingNode):find 去新表 nextTable 找。
-
-2 (TreeBin):find 进红黑树找。
-
-
普通链表:遍历next逐个比较,找到返回,找不到null。
/**
-
根据 key 获取对应的 value。
-
核心特点:无锁(Lock-Free)读取,依靠 volatile 保证可见性。
-
即使有线程正在扩容,get 也能正确找到数据(通过 ForwardingNode 去新表找)。
*/
public V get(Object key) {
// 1. 声明局部变量:tab 引用当前哈希表,e 为桶首节点,p 为查找结果,n 为数组长度
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;// 2. 扰动计算:对 key 的 hashCode 进行高低位混合,减少碰撞
int h = spread(key.hashCode());// 3. 定位桶位,并检查表和桶是否非空
// 关键点:将 table 赋值给局部变量 tab,即使后续 table 被替换(扩容完成),
// 本次 get 依然在旧数组上操作(但旧数组的 ForwardingNode 会引导去新表)
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {// 4. 情况一:桶的首节点正好就是要找的节点(hash 和 key 都匹配) if ((eh = e.hash) == h) { // 4.1 校验 key 的引用相等或 equals 相等 if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; // 直接返回 value } // 5. 情况二:首节点 hash 值小于 0(特殊节点) // - 如果是 ForwardingNode(hash = -1):说明该桶正在扩容,节点已被迁移到新表 // - 如果是 TreeBin(hash = -2):说明该桶已转为红黑树 else if (eh < 0) // 调用节点自身的 find 方法进行查找(多态特性) // ForwardingNode.find -> 去 nextTable 里找 // TreeBin.find -> 在红黑树里遍历查找 return (p = e.find(h, key)) != null ? p.val : null; // 6. 情况三:普通链表结构(eh >= 0),遍历链表进行查找 while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; // 找到则返回 }}
// 7. 未找到任何匹配,返回 null
return null;
}
-
4.2.1、ForwardingNode
/**
* ForwardingNode 是 Node 的子类,hash 固定为 -1(MOVED)
*/
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable; // 指向新表
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
// 重写了 Node 的 find 方法
Node<K,V> find(int h, Object k) {
// 直接拿着新表 nextTable 去查找
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
// 极端情况:新表又发生了扩容,继续往新表跳
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k); // 树节点递归
}
if ((e = e.next) == null)
return null;
}
}
}
}
4.3、addcount
为了不让所有线程争抢同一个计数器,ConcurrentHashMap采用了分段计数思想,使用了两个关键字段:
- baseCount基础计数器:voliatile long类型,记录元素个数的基础值
- CounterCell\[\](计数单元数组):当 baseCount 发生争用时,线程会转而修改自己专属的 CounterCell 单元,把竞争压力分散到多个变量上。
源码定义:
private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;
执行流程:
- 优先尝试 CAS 修改 baseCount:U.compareAndSwapLong(this, BASECOUNT, v, v + x)
- 如果 CAS 失败(说明有竞争):线程会通过 CounterCell 数组进行线程本地计数,每个线程最终只会修改自己映射到的那个 CounterCell 对象
- 扩容联动:addCount 在修改完计数后,还会检查是否达到阈值并触发扩容(您之前看到的 put 线程参与扩容就是从这里进来的)。
java
/**
* 更新元素计数,并视情况触发扩容。
*
* @param x 要增加的数量(通常为 1)
* @param check 若 >= 0,则检查扩容条件;若 <= 1 则可能跳过扩容检查(性能优化)
*/
private final void addCount(long x, int check) {
// ---------- 第一部分:更新计数(baseCount / CounterCell) ----------
CounterCell[] as; long b, s;
// 条件判断:如果 counterCells 已经存在,或者 CAS 更新 baseCount 失败,则进入分段计数逻辑
// 注意:这里利用了短路运算,先判断 as != null,如果为 true 则直接进入 if 体;
// 如果 as == null,则尝试 CAS 修改 baseCount,若失败也进入 if 体。
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true; // 表示是否无竞争
// 尝试将计数累加到当前线程对应的 CounterCell 槽位
// 条件判断(层层递进,失败则调用 fullAddCount 兜底):
// 1. as == null:counterCells 尚未初始化
// 2. (m = as.length - 1) < 0:数组长度为 0(理论上不可能,但防御)
// 3. (a = as[ThreadLocalRandom.getProbe() & m]) == null:当前线程的槽位为空
// 4. !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)):
// 槽位不为空,但 CAS 更新该槽位的值失败(说明有竞争)
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 兜底方法:自旋 + 重新初始化/扩容 counterCells,确保计数成功
fullAddCount(x, uncontended);
return; // 计数完成后直接返回,不再执行后续扩容检查(因为 fullAddCount 内部不检查扩容)
}
// 如果 check <= 1,说明本次 put 没有发生哈希冲突(链表长度为 0 或 1),
// 此时大概率元素总数远未达阈值,跳过扩容检查以提升性能。
if (check <= 1)
return;
// 如果成功更新了 CounterCell,但 check > 1,则需要重新计算总元素数(sumCount),
// 用于后续扩容检查。
s = sumCount();
}
// 如果 CAS 更新 baseCount 成功,则直接使用 s = b + x(已更新后的总数),
// 并继续执行下面的扩容检查。
// ---------- 第二部分:检查是否需要扩容 ----------
// 仅当 check >= 0 时才检查(putVal 中插入新节点时传 binCount,覆盖旧值时不传负值)
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 循环判断条件:
// 1. s(当前元素总数) >= sizeCtl(扩容阈值)
// 2. table 不为空
// 3. 当前数组长度 n < MAXIMUM_CAPACITY(最大容量)
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 生成扩容戳(基于 n 的特定编码,16 位)
int rs = resizeStamp(n);
// 情况一:已有其他线程在扩容(sc < 0)
if (sc < 0) {
// 判断当前线程是否应该加入协助扩容,以下任一条件满足则退出循环不参与:
// 1. 扩容批次戳不匹配((sc >>> RESIZE_STAMP_SHIFT) != rs)
// 说明当前扩容批次与本次要触发的扩容不一致(可能旧扩容已完成)
// 2. sc == rs + 1:表示只剩下一个线程(第一个发起者),扩容即将结束,无需再加
// 3. sc == rs + MAX_RESIZERS:参与线程数已达上限
// 4. (nt = nextTable) == null:新表还未初始化(罕见)
// 5. transferIndex <= 0:所有迁移区间都已被认领完毕
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 通过 CAS 将 sizeCtl 加 1(低 16 位 +1),表示新增一个线程参与扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt); // 参与扩容(协助迁移)
}
// 情况二:没有线程在扩容(sc >= 0),当前线程作为第一个发起者
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2)) {
// 设置 sizeCtl = (rs << 16) + 2,高 16 位为扩容戳,低 16 位为 2(表示有 1 个线程 + 保留位)
transfer(tab, null); // 发起扩容,nextTab 为 null 表示需要新建新表
}
// 更新 s 为最新的元素总数(因为其他线程可能并发修改了计数),继续循环判断
s = sumCount();
}
}
}
4.4、size
/**
* {@inheritDoc}
*/
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
baseCount和CounterCell累加则可以得到容器的长度
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
5、亮点
5.1、锁粒度在桶首节点(减小锁粒度)
- 调用put放入桶节点不为空的数据,只会在当前桶首节点加synchronized
- 扩容过程中,迁移也是按当前桶首节点增加sunchronized
5.2、高低位扩容
- sizeCtl,高16位resizeStamp扩容戳,低16位是参与扩容线程的数量
- put入口遇到MOVED被动的协助扩容
- addCount中容量操作阈值的主动触发扩容
- 理论上就是一个扩容线程负责一段区间,实现上其实就是一个变量等于旧数组的长度,随机计算每个线程可以处理的区间长度最小是16,来一个线程长度较少16,所以他是从索引位置大位置开始迁移
5.3、扩容对读取完全透明
- 正在扩容时,get不需要等待,依旧能瞬间返回正确的值
- 利用多态的find方法。ForwardingNode转发节点重写了find,将查询自动跳转到新表nexttable
5.4、链表迁移批量性能好
- 一个旧链表拆解为低位链表和高位链表,然后原子性地批量插入到新表的位置
- 旧表长度是
n(2 的幂),扩容后新表长度是2n - 旧链表上的数据对应新索引的位置只能为i或者i+n
- 生成两个新链表其实也会是为了保证不修改原链表,保证get功能正常
5.5、addCount分段计数
- 解决了所有线程都去抢一个size变量,CAS会剧烈冲突,性能奔溃
- 让大多数线程只修改自己专属的CounterCell槽位,最后size在汇总