前言:
在此之前我们学习了hashmap,通过源码解析,一步步介绍,在当时我们知道他是线程不安全的,所以在高并发环境下,它没办法使用,这时候就可以使用线程安全的ConcurrentHashmap。
如果还不知道hashmap可以参考我之前写的。https://blog.csdn.net/uybji/article/details/147932178?spm=1001.2014.3001.5501
ConcurrentHashmap简介
数据结构
在 JDK 1.8 中,
ConcurrentHashMap的数据结构与HashMap非常相似:数组 + 链表 + 红黑树 。数组的每个元素是一个Node节点,当链表长度超过阈值(8)时,会转换为红黑树(TreeNode)。但ConcurrentHashMap引入了一些辅助结构来控制并发,例如:
Node类:基础的链表节点。
TreeNode:红黑树节点。
TreeBin:用于包装红黑树的根节点,充当锁的角色。
ForwardingNode:扩容时出现在旧数组中的特殊节点,表示该桶已迁移。
重要的成员变量与常量


初始化
ConcurrentHashMap 采用懒加载方式,构造时只会设置一些参数(如初始容量、负载因子),真正的数组创建发生在第一次插入操作时。和hashmap是一样的。

这里的
sizeCtl被设置为大于 0 的数,表示下一次扩容的阈值(类似HashMap的threshold)。注意tableSizeFor方法用于计算大于等于给定值的最小 2 的幂。
put 操作
这是它的核心操作,我们根据源码来一点点分析

同样调用put之后会调用putval,接下来看看他的源码
java
```java
/**
* ConcurrentHashMap的核心put方法实现,支持普通put和putIfAbsent操作
* 实现了线程安全的键值对存储,支持高并发环境下的高效操作
*
* @param key 要插入的键,不能为null
* @param value 要插入的值,不能为null
* @param onlyIfAbsent 如果为true,则只有当键不存在时才插入/更新值
* 如果为false,则无论键是否存在都会插入/更新值
* @return 如果键已存在,返回旧值;如果键不存在或onlyIfAbsent为true且键存在,返回null
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ConcurrentHashMap不允许null键和null值
if (key == null || value == null) throw new NullPointerException();
// 计算键的哈希值,使用spread方法增强哈希分布,减少冲突
int hash = spread(key.hashCode());
// 用于记录当前桶中节点数量,用于后续可能的链表转红黑树判断
int binCount = 0;
// 无限循环,确保操作最终成功(CAS失败等情况会重试)
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
// 情况1:哈希表尚未初始化或为空
if (tab == null || (n = tab.length) == 0)
// 初始化哈希表
tab = initTable();
// 情况2:计算键在表中的位置,且该位置为空桶
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 使用CAS操作无锁插入新节点
// 成功则跳出循环,失败则重试(可能有其他线程已插入)
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // 空桶插入不需要加锁
}
// 情况3:当前桶的头节点是MOVED状态,表示正在扩容
else if ((fh = f.hash) == MOVED)
// 帮助完成扩容,返回扩容后的新表
tab = helpTransfer(tab, f);
// 情况4:检查是否满足putIfAbsent的快速返回条件
// 仅在onlyIfAbsent为true时检查,无需加锁,提高效率
else if (onlyIfAbsent // 仅当onlyIfAbsent为true时才进入此分支
&& fh == hash // 哈希值匹配
&& ((fk = f.key) == key || (fk != null && key.equals(fk))) // 键匹配
&& (fv = f.val) != null) // 值不为null
// 键已存在且值不为null,直接返回旧值,不进行更新
return fv;
// 情况5:需要对桶进行加锁操作(链表或红黑树操作)
else {
V oldVal = null;
// 对当前桶的头节点加锁,确保线程安全
synchronized (f) {
// 双重检查:确保头节点未被其他线程修改
if (tabAt(tab, i) == f) {
// 子情况5.1:头节点的哈希值>=0,表示是普通链表结构
if (fh >= 0) {
binCount = 1; // 链表节点计数从1开始
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到匹配的键
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val; // 记录旧值
// 如果不是onlyIfAbsent模式,更新值
if (!onlyIfAbsent)
e.val = value;
break; // 找到键,跳出循环
}
Node<K,V> pred = e;
// 到达链表末尾,插入新节点
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break; // 插入完成,跳出循环
}
}
}
// 子情况5.2:头节点是TreeBin,表示是红黑树结构
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2; // 红黑树的binCount从2开始
// 在红黑树中插入或更新节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val; // 记录旧值
// 如果不是onlyIfAbsent模式,更新值
if (!onlyIfAbsent)
p.val = value;
}
}
// 子情况5.3:头节点是ReservationNode(保留节点)
// 这种情况不应该发生在常规put操作中
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 操作完成后检查
if (binCount != 0) {
// 如果链表长度达到树化阈值,将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
// 如果找到了旧值,返回旧值
if (oldVal != null)
return oldVal;
// 插入了新节点,跳出循环
break;
}
}
}
// 更新元素计数,并可能触发扩容
addCount(1L, binCount);
// 插入了新节点,返回null
return null;
}
```
注意:在这个时候我们对比hashmap和它的计算hash值的过程可以发现(上面是hashmap)
它多了一个& HASH_BITS这个操作,这是因为,在concurrenhashmap中hash值会有负数,来表示一些情况所以要进行这个操作防止hash值为负数产生混淆:
MOVED= -1(表示节点正在迁移)
TREEBIN= -2(表示树根节点)
RESERVED= -3(表示占位节点)
我们一点点来分析

进行非空判断,这是因为concurrenhashmap不支持null值(这是因为在高并发环境下,get获取值为null的话无法分辨到底是key不存在所以返回null,还是因为值就是null,防止产生两异性,并且我们可以看见上面计算hash值时,hashmap是判空了的而它没有)
然后进行hash值计算,binCount表示数组桶的个数。

第一个if我们进行数组初始化,在刚刚说了初始化concurrehashmap的时候我们是没有创建数组的,当第一次调用put时才会进行数组初始化。
第二个else if 表示头节点为空的话直接用CAS进行插入如果成功直接break跳出循环。
i = (n - 1) \& hash)表示数组下标
tabAt(tab, i = (n - 1) \& hash))根据下标获取数组头节点,tabAt是java Usafe包中提供的方法,后面会介绍。第三个else if 表示算出来的hash值为-1,这个时候表示现在数组在进行扩容,当前线程会直接调用
进行协助扩容,因为现在是多线程。
第四个else if表示在
ConcurrentHashMap的putVal方法中,onlyIfAbsent参数的含义是:当且仅当键不存在时才插入 。如果键已存在,则不进行任何更新,直接返回已有的值。

可以看见现在加锁了,所以才说它是线程安全的,不过这个锁是一个桶级锁,只会对数组其中一个点生效,其他点是不影响的。
双重判定,当拿到锁之后,还要拿一次头节点与现有的头节点比较,这是因为多线程,我们在拿到锁的一瞬间也许,其他线程就把当前头节点改变了,使得最新的数组结构可能就不再是链表而是红黑树,这时候如果当前线程继续按旧结构去操作,这会产生数据结构破坏。
现在看到的就是操作链表的过程,为什么当fh>0就表示链表,因为fh这时候是hash值,在最开始就对hash值做了处理不可能为负数,而树结构的fh是-2,所以只能是链表。

剩下的两个情况分别是树结构的处理,和表示头节点是保留节点,但是在put中不可能有这种情况所以直接抛异常

进行判断,如果bincount>8表示要转换为树结构,因为bincount表示的是链表个数。
大家注意虽然已经调用了,但是它里面会判断,要是数组容量<64的时候还是会优先扩容而不是转变为树。

它有两个作用一个是更新数组元素个数,一个是进行扩容判断。
sizeCtl的含义
sizeCtl 是 ConcurrentHashMap 中一个至关重要的控制变量,用于协调多线程的初始化和扩容操作。它的值在不同阶段代表不同含义,设计精巧,是理解并发扩容机制的关键。
负数状态:表示正在进行初始化或扩容
-1:表示当前有线程正在初始化哈希表(table数组)------即initTable()方法正在执行,其他线程遇到此值会自旋等待。其他负数(< -1):表示哈希表正在扩容,具体数值由两部分组成:
高 16 位:扩容标识戳,基于当前容量生成,用于标识本次扩容的唯一性。
低 16 位:参与扩容的线程数加 1
例如,若
sizeCtl = (resizeStamp << 16) + 2,表示当前容量为 n 的扩容正在进行中,已有一个线程在迁移数据;当该值变为(resizeStamp << 16) + 3时,说明有第二个线程加入。
2. 正数状态:表示阈值或初始容量
table未初始化 :sizeCtl存储的是构造时传入的初始容量(如果传入的是 0,则暂为 0,后续在第一次put时会被设为默认值16对应的阈值)。
table已初始化 :sizeCtl存储的是下一次触发扩容的阈值,计算公式为容量 * 负载因子(负载因子固定为 0.75)。当元素个数超过该值时,将触发扩容。
3. 零状态
- 表示构造时未指定初始容量,且
table尚未初始化。在第一次插入时,sizeCtl会被设置为默认容量(16)对应的阈值(12)。
addCount源码
java
```java
/**
* Adds to count, and if table is too small and not already
* resizing, initiates transfer. If already resizing, helps
* perform transfer if work is available. Rechecks occupancy
* after a transfer to see if another resize is already needed
* because resizings are lagging additions.
*
* @param x the count to add (通常为1,表示新增一个元素)
* @param check if <0, don't check resize (不检查扩容), if <= 1 only check if uncontended (仅在无竞争时检查扩容)
*/
private final void addCount(long x, int check) {
CounterCell[] cs; long b, s;
// 阶段1: 并发计数逻辑 - 采用两阶段计数策略提高并发性能
// 策略1: 优先使用baseCount进行计数 (无竞争时效率最高)
// 如果counterCells不为空,或者CAS更新baseCount失败(发生竞争),则进入策略2
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell c; long v; int m;
boolean uncontended = true; // 标记是否无竞争
// 策略2: 使用CounterCell数组分散计数 (有竞争时使用)
// 检查条件:
// 1. counterCells数组为空
// 2. 数组长度小于等于0 (无效)
// 3. 当前线程映射到的CounterCell为空 (首次访问)
// 4. CAS更新CounterCell的值失败 (该Cell发生竞争)
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended = // 尝试CAS更新CounterCell的值,并记录是否成功
U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
// 策略3: 极端竞争情况,调用fullAddCount处理 (创建/扩容CounterCell数组)
fullAddCount(x, uncontended);
return;
}
// 如果check <= 1且当前是无竞争情况,不进行扩容检查
// 这种情况通常发生在对同一个桶的连续操作,避免不必要的扩容检查
if (check <= 1)
return;
// 计算当前元素总数:baseCount + 所有CounterCell的值之和
s = sumCount();
}
// 阶段2: 扩容检查与触发逻辑
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 循环检查扩容条件:
// 1. 当前元素总数s >= 扩容阈值sc
// 2. 哈希表不为空
// 3. 当前容量小于最大容量(2^30)
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 生成扩容标记:将当前容量n传入resizeStamp生成唯一标记,然后左移RESIZE_STAMP_SHIFT位
// 该标记用于后续线程间的扩容状态协调
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
// 情况1: 已有线程在扩容 (sc < 0表示扩容状态)
if (sc < 0) {
// 扩容退出条件检查:
// 1. sc == rs + MAX_RESIZERS: 达到最大协助线程数
// 2. sc == rs + 1: 扩容即将完成 (所有桶已分配)
// 3. nextTable == null: 扩容已结束
// 4. transferIndex <= 0: 没有更多桶需要迁移
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break; // 无法协助扩容,退出循环
// 尝试CAS将sc加1,表示当前线程加入扩容队伍
// 成功则调用transfer方法协助扩容
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt); // 协助已有扩容操作
}
// 情况2: 无其他线程在扩容,尝试触发新的扩容
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null); // 首次触发扩容,nextTable为null
// 扩容后重新计算元素总数,防止扩容过程中元素增长过快需要再次扩容
s = sumCount();
}
}
}
```
第一个功能,数组元素增加
扩容功能判定
helpTransfer源码
java
/**
* 协助ConcurrentHashMap进行并发扩容的核心方法
*
* 当线程在操作哈希表时遇到ForwardingNode节点(表示正在进行扩容),
* 会调用此方法参与到扩容过程中,提高扩容效率
*
* @param tab 当前的哈希表数组
* @param f 当前访问到的节点,通常是一个ForwardingNode
* @return 扩容后的新哈希表数组,如果扩容未完成则返回当前表
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 检查扩容条件是否满足:
// 1. tab不为空
// 2. 当前节点f是ForwardingNode实例(表示正在扩容)
// 3. 能够从ForwardingNode中获取到有效的新表nextTab
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 生成扩容标记:使用当前表长度计算resizeStamp,然后左移RESIZE_STAMP_SHIFT位
// 此标记用于后续线程间的扩容状态协调和验证
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
// 循环条件:
// 1. nextTab == nextTable:确保扩容仍在进行(新表未被替换)
// 2. table == tab:确保当前表未被其他线程替换
// 3. sizeCtl < 0:确保仍处于扩容状态
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 退出循环条件检查:
// 1. sc == rs + MAX_RESIZERS:已达到最大协助线程数
// 2. sc == rs + 1:扩容即将完成(所有桶已分配完毕)
// 3. transferIndex <= 0:没有更多桶需要转移
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
transferIndex <= 0)
break;
// 使用CAS操作尝试将sizeCtl加1,表示当前线程加入扩容团队
// 成功则调用transfer方法执行实际的数据迁移工作
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab); // 执行数据迁移
break; // 完成协助后退出循环
}
}
// 返回扩容后的新表
return nextTab;
}
// 未满足扩容条件或扩容已完成,返回当前表
return table;
}
transfer源码非常重要
因为我们在面试的时候一般也是问的扩容机制,扩容其实也就是创建一个新数组,然后把旧数组的元素迁移到新数组,它就是扩容的主要实现,接下来我们一点点来看
java
/**
* ConcurrentHashMap的核心扩容方法,负责将旧表中的节点迁移/复制到新表
* 支持多线程并发扩容,提高扩容效率
*
* @param tab 旧的哈希表数组
* @param nextTab 新的哈希表数组(容量是旧表的2倍),首次调用时为null
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 计算每个线程处理的桶数量:
// - 多核CPU下:(n >>> 3)/NCPU 即 n/8/NCPU,确保每个CPU核处理的桶数大致均匀
// - 单核CPU下:n(整个表由一个线程处理)
// - 确保最小处理粒度不小于MIN_TRANSFER_STRIDE(默认为16),避免过多线程竞争
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // 保证最小处理粒度
// 初始化阶段:如果nextTab为null,表示当前线程是第一个发起扩容的线程
if (nextTab == null) { // initiating
try {
// 创建新表,容量是旧表的2倍(n << 1)
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // 处理OOM异常(内存不足)
sizeCtl = Integer.MAX_VALUE; // 设置一个极大值,标记扩容失败
return;
}
nextTable = nextTab; // 设置全局变量,通知其他线程正在进行扩容
transferIndex = n; // 设置迁移索引,从旧表末尾开始分配任务
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // 创建转发节点
boolean advance = true; // 是否需要推进到下一个桶
boolean finishing = false; // 是否完成扩容的最后阶段
// 主循环:处理桶的迁移任务
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 任务分配阶段:获取当前线程需要处理的桶范围
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false; // 已经处理完当前分配的桶或正在完成阶段
else if ((nextIndex = transferIndex) <= 0) {
i = -1; // 没有更多桶需要处理
advance = false;
}
// 使用CAS原子更新transferIndex,分配stride个桶给当前线程
else if (U.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound; // 当前线程处理的桶的下界
i = nextIndex - 1; // 当前线程处理的第一个桶(从后往前)
advance = false;
}
}
// 边界检查和完成处理
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // 完成扩容的最后阶段
nextTable = null; // 清空临时变量
table = nextTab; // 更新全局表引用为新表
// 设置新的sizeCtl:(2n) - (n/2) = 1.5n,作为下一次扩容的阈值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 减少正在扩容的线程计数
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 检查是否所有线程都已完成扩容
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 进入完成阶段
finishing = advance = true;
i = n; // 重新检查所有桶
}
}
// 桶为空:直接放置转发节点
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 桶已经被处理过:跳过
else if ((fh = f.hash) == MOVED)
advance = true; // 已被其他线程处理
// 处理非空桶
else {
synchronized (f) { // 对桶的头节点加锁,保证线程安全
if (tabAt(tab, i) == f) { // 双重检查头节点是否未被修改
Node<K,V> ln, hn; // 低位链表和高位链表
// 处理普通链表节点(fh >= 0)
if (fh >= 0) {
int runBit = fh & n; // 计算节点在新表中的位置(0或n)
Node<K,V> lastRun = f; // 记录最后一个连续相同runBit的节点
// 查找lastRun:从链表中找到最后一个连续相同runBit的节点
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 根据lastRun的runBit确定低位和高位链表
if (runBit == 0) {
ln = lastRun; // ln指向lastRun,hn为null
hn = null;
}
else {
hn = lastRun; // hn指向lastRun,ln为null
ln = null;
}
// 将链表节点分配到低位或高位链表
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); // 头插法构建高位链表
}
// 将低位链表放入新表的i位置
setTabAt(nextTab, i, ln);
// 将高位链表放入新表的i+n位置
setTabAt(nextTab, i + n, hn);
// 在旧表的i位置放置转发节点,标记该桶已处理
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; // 低位和高位节点计数
// 将树节点分配到低位或高位链表
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);
// 在旧表的i位置放置转发节点,标记该桶已处理
setTabAt(tab, i, fwd);
advance = true; // 继续处理下一个桶
}
// 处理预留节点(不应该出现在这里)
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
}
}
}
看着很大一串,我们来一部分一部分的看

这一部分其实就是计算每个线程的工作量,因为我们知道线程会进行协助扩容,那每个线程应该如何分配,就是靠这个,但是每个线程最少也要分到16个桶。

这段其实就是当第一个线程进入扩容时,他会创建一个新数组,也就是第一次扩容时。

这段其实就是更细致的任务分配,刚刚算的每个线程最少不低于16个桶,这里就是计算每个线程从哪个节点到哪个节点,工作范围

这一段,两个if,第一个if表示获取到的头节点为空时,直接给adcance赋值fwd,这个时候表示此点已经被转移完毕,可以忽略了,那么此时线程就会前往下一个节点。就相当于一个记号。
第二个if表示此节点hash为-1,我们说过-1表示正在扩容,所以此时这个点已经有线程在工作,那么其他线程就可以前往其他点。

这个就是结尾工作,对每个桶进行检查,还会把sizeCtl设置为下一次的扩容阈值。
java
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & 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;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
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);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
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;
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;
}
}
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;
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
这段代码其实就是对链表结构或者树结构进行数据迁移的,把他们从旧数组给弄到新数组。在hashmap中说过,迁移到坐标是根据每个点的hash值对旧数组容量进行按位与操作,当结果为零,那么新位置就是原位置,否则新位置就是hash值+旧容量,不理解可以去我之前写的看看。
tabAt casTabAt setTabAt
tabAt:保证你拿到的就是最新的
casTabAt:保证数据不会被直接覆盖,比如在更新时,当一个线程要更新,会查看此时的点还是不是之前旧的那个,如果不是就说明已经被其他线程修改,那么它就会重新进行,put的流程,就不会把别人刚刚修改的数据给覆盖,保证了线程安全。它就是CAS的具体实现。
setTabAt:确定要覆盖时用,保证"写完别人立刻看见"
他们三个就是线程安全的实现者,在concurrenthashmap的线程安全中,就是靠他们实现了无锁线程安全,所以呀concurrenthashmap加锁的场景很少,性能也比较好。
ConcurrentHashMap扩容机制
两种情况第一是当链表大于8但是数组小于64,第二种就是元素个数超过了阈值
ConcurrentHashMap 的扩容机制是多线程协同迁移,核心流程如下:
触发时机 :每次 put 操作后,元素个数超过
sizeCtl(容量×0.75)时触发扩容。新数组创建:第一个触发扩容的线程创建新数组,大小为旧数组的两倍。
任务分片 :有一个全局的
transferIndex指针,从旧数组末尾开始向前分配任务。每个线程通过 CAS 竞争领取一段连续的桶(默认最小 16 个),领取后负责迁移这段区间内的所有桶。桶迁移:
对每个桶的头节点加锁(synchronized),防止并发修改。
根据桶的类型(链表或红黑树)将节点分为低位链 (留在原索引)和高位链(移到原索引+旧容量)。
迁移后将高低位链放入新数组对应位置,并在旧数组的桶位置放置一个
ForwardingNode标记,表示该桶已处理。并发读 :读操作遇到
ForwardingNode时,直接去新数组中查找,不影响读。写操作协助 :如果 put 时发现桶头是
ForwardingNode,当前线程会先协助扩容,搬完桶后再继续自己的插入。完成收尾 :所有桶迁移完后,最后一个退出扩容的线程将新数组赋值给
table,并重新计算sizeCtl为新阈值。
与hashmap的对比


总结:
我们通过源码解释了concurrenthashmap,大家在理解的时候对照着hashmap进行学习,最好自己也可以独自去分析一下源码看看怎么样,谢谢你的阅读,一起加油吧!



进行协助扩容,因为现在是多线程。


