前言
水平有限,尽量深入
主要关注的点
- put 方法相关
- put 方法做了哪些事
- 如何保证并发 put 安全(cas 和 synchronized 的使用)
- 扩容相关
- 扩容过程
- 扩容如何保证并发安全性
- get 方法线程安全
- size 机制
使用 ConcurrentHashMap 中的一些疑问解析
put 方法相关
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 均不能为 null
if (key == null || value == null) throw new NullPointerException();
// 计算 hash
int hash = spread(key.hashCode());
int binCount = 0;
// 进入一个无限循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果没有进行初始化,则首先进行初始化
if (tab == null || (n = tab.length) == 0)
// 通过 cas 操作,保证初始化的并发安全
tab = initTable();
// 如果要put 的 key 要放置的桶为空,则直接将new 的 node ,利用 cas 操作设置为头结点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果当前 table 正在扩容,那么当前线程需要帮助进行扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 锁住头结点,这样其他线程就无法操作这个桶上所有的 node
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果 key 已经存在,则覆盖value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
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, null);
break;
}
}
}
// 如果头结点是树节点,则在红黑树中插入
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// binCount >= 8,则代表当前的链表需要转换为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// count + 1,并扩容
addCount(1L, binCount);
return null;
}
put方法流程:
- key value 的 null 处理
- 计算 hash
- 如果 table 没有初始化,则需要初始化
- 如果 key 对应的桶为空,则需要 cas 将新节点作为头结点放在在桶中
- 如果当前 table 正在扩容,那么当前线程需要帮忙扩容
- 如若没有进 3.4.5分支,则需要根据 hashcode 判断是 replace 或者是插入到链表/树中
- 如果是插入操作
- 如果是插入链表,则还需要判断是否转化成红黑树
- 插入之后,需要通过 addCount 进行 count 的加一,以及扩容操作
并发环境下,put操作如何保证线程安全:
- 如果 key 对应的桶中,还没有存放节点,那么使用 cas 操作,将首节点设置到桶中。多线程条件下,只有一个线程能通过 cas 设置首节点。
- 如果 key 对应的桶中,已经存放了一些节点,那么通过对首节点进行 synchronized 操作,保证同一个桶,同一个时间点,只有一个线程在操作。
扩容流程:
有两个操作会引发扩容:
- 当桶中节点由链表结构转换为红黑树时,treeifyBin 操作
- 当插入节点后,addCount 操作
- 如果一个线程在扩容过程中,另外一个线程要插入数据,则需要帮助扩容,进行 helpTransfer 操作
这三个操作进行扩容,核心都是调用方法:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
分析具体怎么扩容之前,需要了解ConcurrentHashMap 很重要的一个参数:sizeCtl
这个参数在初始化table、扩容的过程中都有涉及到:
(1)、sizeCtl 为 -1:初始化过程中
U.compareAndSwapInt(this, SIZECTL, sc, -1)
作用:将 sizeCtl 值设置为 -1 表示集合正在初始化中,其他线程发现该值为 -1 时会让出CPU资源以便初始化操作尽快完成 。
(2)、sizeCtl > 0:初始化完成后
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
sizeCtl = sc;
作用:sizeCtl 用于记录当前集合的负载容量值,也就是触发集合扩容的极限值 。
(3)、sizeCtl <0:正在扩容时
//第一条扩容线程设置的某个特定基数
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
//后续线程加入扩容大军时每次加 1
U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)
//线程扩容完毕退出扩容操作时每次减 1
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
作用:sizeCtl 用于记录当前扩容的并发线程数情况,并发线程数可以通过 (sizeCtl & 0xFFFF)-1 来获取。
注意:这里有一些疑问,sizeCtl 参数的注释里写的是,当 sizeCtl < 0的时候,并发线程数是 n 的话,sizeCtl = -(n+1)。这里感觉有点问题,我自己看代码的时候,确实不是注释中写的那样子。第一条扩容线程设置的 siceCtl(计算方式为:U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2) ),其实并不是-2,而是很大的一个负数。这个原因在于扩容时,sizeCtl初始化的方式:(rs << RESIZE_STAMP_SHIFT) + 2,而 rs 的计算方式是 Integer.numberOfLeadingZeros(table.length) | (1 << (RESIZE_STAMP_BITS - 1))。通过这些位运算之后,其实 rs 的低 16 位就是当前扩容的线程数+1
下面看下transfer 方法做了什么:
- 根据 CPU 计算每个扩容线程分配的桶的数量
- 在一个 for循环中进行桶的迁移
- 首先是利用一个 while 循环,为当前线程分配自己需要迁移的桶的区间
- 跳出 while 循环后,首先判断本线程的是否还有迁移任务需要做,如果没有的话,需要判断本线程是不是最后一个负责迁移的线程,如果是的话,需要做一些收尾工作(置空成员变量 nextTable,更新成员变量 table 为新的数组等);否则,直接 reurn,结束本线程的 transfer 方法。
- 然后需要判断当前迁移的桶的头结点是否为null,为 null 的话,直接插入 ForwardingNode 进行占位
- 然后判断当前节点是否已经被迁移了,是的话直接跳过这个桶
- 如果当前节点需要进行迁移的话,先把桶的头结点锁掉,然后进行迁移
链表迁移过程:- 首先遍历链表,取到链表的尾结点,并得到尾结点是高位还是低位
- 然后通过头插法,产生分别由高位节点和低位节点组成的两个链表,然后把低位链表的头结点设置到下标为 i(i 是该节点在原数组的下标),把高位链表的头结点设置到下标为 i+n (i 是该节点在原数组的下标,n 是原数组的长度)
transfer 具体代码:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// stride 代表扩容时,每个扩容线程的「步」,即每个线程最大迁移的桶的数量
int n = tab.length, stride;
// 多核CPU情况下,stride = tab长度 * 8 / 核数,否则stride = table 长度,且stride 最小是 16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 如果不指定扩容后的数组长度,则直接扩容为原来的两倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// 下一个要迁移的索引(加一之后的值),等于 n 则表示,要从旧表的末尾开始迁移数据到新表
transferIndex = n;
}
int nextn = nextTab.length;
// 在扩容的过程中,如果有其他线程尝试进行读操作,那么通过 ForwardingNode 将读操作转发到新的 table 上去
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 是否能够向前推进到下一个区间,首次为 true
boolean advance = true;
// 完成状态,如果是 true,就结束此方法
boolean finishing = false; // to ensure sweep before committing nextTab
// i 指代当前需要处理的桶的下标;bound 表示当前线程可以处理的当前桶区间最小下标
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
// 如果当前区间还未处理完成,则将 advance 置为 false,跳出 while,继续处理当前区间
if (--i >= bound || finishing)
advance = false;
// 如果当前区间已经处理完成,而且不存在其他区间可以分配,则将 i 置为-1,将 advance 置为 false
else if ((nextIndex = transferIndex) <= 0) {
// 当 transferIndex <=0,则表示已经没有需要迁移的桶,这时候,将 i 置为 -1,准备退出迁移工作
i = -1;
advance = false;
}
// 如果当前区间已经处理完成,而且存在其他区间可以分配,则申请下一个可分配的区间,然后将 advance 置为false,跳出 while,进行迁移
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 将边界置为 nextIndex - stride 或者 0
bound = nextBound;
// 从最右边开始处理
i = nextIndex - 1;
advance = false;
}
}
// i<0 代表没有需要迁移的桶
// TODO i >= n ???
// TODO i + n >= nextn ???
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 扩容完成
if (finishing) {
// 清空成员变量
nextTable = null;
// 替换为新的 table
table = nextTab;
// 更新 sizeCtl
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//表示当前线程迁移完成了
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 如果当前线程是迁移工作不是最后一个线程,则直接 return,结束当前线程的工作
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 如果当前线程是迁移工作的最后一个线程,则将 finishing 标记置为true,标记整个迁移工作已经结束
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果当前迁移桶的头结点为null,则直接使用 ForwardNode 进行占位
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 当前节点已经处理过了
else if ((fh = f.hash) == MOVED)
advance = true;
// 进行迁移工作
else {
// 对节点上锁,防止其他线程 putVal
synchronized (f) {
if (tabAt(tab, i) == f) {
// ln 高位桶;hn 低位桶,分别保存hash值的第X位为0和1的节点
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;
}
}
// 如果最后一个节点是低位,则将 lastRun 赋值给 ln,负责将 lastRun 赋值给 hn
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 遍历链表,根据高低位,以 ln,hn 为尾结点,进行头插
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);
}
// 设置低位 node 组成的链表的头结点
setTabAt(nextTab, i, ln);
// 设置高位 node 组成的链表的头结点
setTabAt(nextTab, i + n, hn);
// 原 table 的节点使用 FowardingNode 占位
setTabAt(tab, i, fwd);
advance = true;
}
// TODO 红黑树以后再补充
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;
}
}
}
}
}
}
transfer 方法迁移过程相关示意图:
扩容过程中的其他线程操作行为:
- put 方法
- 需要帮助一起扩容,扩容完之后才能进行put操作
- get 方法
- 借助volatile,不需要加锁。不过需要通过 ForwardingNode 转换到新的 table 上去进行 get 操作。
get 方法线程安全
get 方法不需要加锁,原因是 Node<K,V>[] table 和 Node对象中的 val 和 next 字段都是 volatile 修饰的,不存在脏读的问题。
size 方法是如何在其他线程插入的时候计算 size 的
size 机制建立在这两个成员变量上,相关方法是 addCount 和 size.
private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;
在 addCount 中,会选择首先 cas 更新 baseCount,如果操作失败,说明并发竞争比较高,这时候,将新增的数量放在 CounterCell 之中。size 方法计算时,则取 baseCount + (CounterCell[]中所有 value 的和) 作为 ConcurrentHashMap 的数量返回。
size 相关源码:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
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;
}
addCount 相关源码:
// TODO
参考
- https://blog.csdn.net/ZOKEKAI/article/details/90051567