在上上篇文章《HashMap 与 ConCurrentHashMap的简单原理》中,
笼统介绍了,这两个Map 共同的数据结构。
在上篇《HashMap 源码解析》,详细解析了HashMap 的源码。
本篇分析 ConCurrentHashMap
的源码,侧重讲解与 HashMap
不同的地方。
如果前两篇文章不熟悉,出门左拐,先看那两篇。
本文源代码取 java 1.8
版本。
先提醒下,本文分析的超级详细,文章特别的长!!
一、添加元素
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) {
if (key == null || value == null) throw new NullPointerException();
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)
tab = initTable();
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
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 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;
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
if (key == null || value == null) throw new NullPointerException();
这行说明它与HashMap 的一点不同。
ConCurrentHashMap
key 和 value 都不可以是null,而 HashMap
则无此限制。
int hash = spread(key.hashCode());
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
这段是它的 哈希函数,也就是求数组下标的,解析 HashMap
源码时讲过。
不明白可以看《hash & (n - 1)》。
HASH_BITS = 0x7fffffff
; 这个数字,转化为二进制,是31个1,
和它进行与运算,那也那结果一定大于0。这个很重要!!
正常结点的 hash 大于 0 。
- 初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
这段是数组为空,初始化数组,相当于上节讲的 resize()
方法,等会再详细说。
- 目标位置为空,直接设置
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
}
(n - 1) & hash
这个是哈希函数,用来算下标,上篇讲过。
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
这两个方法,是 直接操作 Unsafe
类,
tabAt
是返回数组指定下标的元素,
casTabAt
是 CAS 方式,在指定下标处设值。
这里再讲的仔细点 ((long)i << ASHIFT) + ABASE
这个算出来是什么?
Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak); // 起始位置
int scale = U.arrayIndexScale(ak); // 一个元素的大小(int 4字节,long 8 字节)
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
((long)i << ASHIFT) + ABASE // 相当于数组的寻址公式
在《为什么数组下标从0开始》,这篇文章中, 说过,
数组的寻址公式是 a[i]_address = base_address + i*data_type_size
。
public static void main(String[] args) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe U = (Unsafe) f.get(null);
Class<String[]> ak = String[].class;
int base = U.arrayBaseOffset(ak);
log.info("base:{}", base); // 16,即起始是16
int scale = U.arrayIndexScale(ak);
log.info("scale:{}", scale); // 4,即偏移量是 4
int shift = 31 - Integer.numberOfLeadingZeros(scale);
log.info("shift:{}",shift); // 2
for(int i = 0; i < 5; i++){
long result = ((long) i << shift) + base; // i 扩大4倍,加上 base
log.info("result:{}",result);
}
}
我写了个demo,来模拟这个过程, String 类型的数组,
((long) i << shift) + base;
在本例中就是 i << 2 + 16
寻址公式,应该是 16 + i * 4 这俩一个效果。
base 为什么是16?
数组对象,对象头8字节、指针4字节、数组长度 4字节。所以从16开始。
其实 new 一个数组对象出来,内存会开辟一块连续的空间,
前面是对象头、指针、记录长度,最后才是数据。
啰啰嗦嗦讲这么多,(Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE)
这个本质是寻址公式,也就是 tab[i]。
HashMap
用的是 tab[i]
,简单明了,明明白白。
ConCurrentHashMap
用的 native Object getObjectVolatile(Object var1, long var2);
效果都是查看数组某下标处的元素,后者更多是从并发角度来考虑的。
transient volatile Node<K,V>[] table;
虽然用了 volatile
,线程间可见,
网上说,数组是线程间可见,但数组元素未必。
ConCurrentHashMap
从并发角度考虑,用了更为底层的方法来查看元素。
.
- 插入元素遇到扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
static final int MOVED = -1; // hash for forwarding nodes
这里先记住,当 hash 值是 -1时,说明正在扩容。
也就是说,插入元素时,正好在扩容,就调用 helpTransfer(tab, f); 一起扩容
即A线程触发了扩容,此时B线程插入元素,
那么B线程和A线程一起来完成扩容。
开始我看这段的时候,也懵,B线程来插入元素的,跑去扩容,那还插入不?
当然B还是要插入的,为什么?
for (Node<K,V>[] tab = table;;) {
……
}
翻上去看下,这是个无限循环。
B参与扩容之后,会再循环,最终肯定会执行它的插入操作。
helpTransfer(tab, f);
这个帮助扩容的方法,等会再细讲。
.
- 存在哈希冲突
else {
V oldVal = null;
synchronized (f) {
……
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
遇到哈希冲突时,代码的逻辑与 Hashmap
的差不多,要么按链表处理,要么按红黑树处理。
不同的是有 synchronized
关键字,即加锁处理。
f 是什么?前面说了 f = tabAt(tab, i = (n - 1) & hash)
是数组中该下标的元素。
在《HashMap 与 ConCurrentHashMap基本原理》中,说过其加锁的事儿,
这个粒度很细,对数组某下标元素加锁,不影响数组的其它位置。
即兼顾效率,又保证安全性。Doug Lea
真牛。
addCount(1L, binCount);
这行代码类似是扩容,等会儿再细讲。
至此,put()
方法大逻辑讲完了,与 HashMap
极其相似。
其中并发作了充分的控制,总结下有以下几点
- 初始化会并发控制
- 扩容会并发控制
- 查看数组某下标元素,使用 Unsafe 类中的 native 方法
- 扩容遇到并发,协助扩容
- 哈希冲突时,对相应数组下标元素加锁
二、数组初始化
上面说过,put
方法招行时,若数组未初始化,会调用 initTable()
方法
if (tab == null || (n = tab.length) == 0)
tab = initTable();
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(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
这里有一个全局变量,是用来标识初始化的
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
如果 sizeCtl = -1,说明是在初始化,如果 -(n+1)
说明有 n
个线程在扩容。
ConcurrentHashMap 初始化时,会设置sizeCtl
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
看过上篇《HashMap源码分析》,这段代码应该能看懂, sizeCtl 是 2 的 n 次方
明白了这些,下面这段就不用解释了
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
接着看下句 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1))
这是用 CAS 方法,将参数 sizeCtl
设置为 -1
,若不成功,则进入下一次的循环。
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
这段就很好理解了,sc 是 2 的 n 次方,new 一个以此为大小的数组(大小为n)。
之后 sc 设置为 n 的 3/4,最后 sc 赋值给参数 sizeCtl
。
也就是说,数组初始化后,容量一定是 2 的 n 次方, sizeCtl
是容量的 3/4。
此时,sizeCtl
是扩容的阈值,等会儿会讲到。
三、触发扩容
在 put 方法最后一段 addCount(1L, binCount);
是可能触发扩容 。
另外、链表转红黑树时,若数组容量小于64,也会触发扩容。(hashMap
讲过)
网上讲扩容的文章,大把。可讲触发扩容,很少。
基本上一句话带过,或压根不讲,可能因为这段比不好讲吧。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
这段代码不多,可极其复杂,只讲 put 方法调用时,它是怎么触发扩容的。
咱只说第一个触发扩容的那个线程。其它的后面再说。
put 方法在最后调用 addCount(1L, binCount);
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
……
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
……
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
s = b + x
这个是当前存入 key-value的总数。这个先记住,后面再解释。
if (check >= 0)
这个肯定满足。
while (s >= (long)(sc = sizeCtl)
这个条件是,达到扩容的阈值了。
前面说过,数组初始化之后,sizeCtl
是扩容的阈值。
if (sc < 0) {}
这个先不考虑,直接看下面,触发扩容。
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
这段的意思就是,将 参数 sizeCtl
设置为某个值,设置成功就执行 transfer(tab, null);
简单的说,就是在某种情况下,启动扩容。
sizeCtl
在执行数组初始化时,会设置为 -1,
初始化完成时,会设置为扩容阈值,
扩容时,会是负数,并会记录有几个线程在扩容。
下面咱们看下,扩容时, sizeCtl
是怎么设置的。
rs << RESIZE_STAMP_SHIFT) + 2
,这个值为负,低16位是 0000 0000 0000 0010
我摘录几段代码,看了就清楚了。
//第一条扩容线程设置的某个特定基数
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)
第一行代码先别问为什么,记住,等会说。
这三行代码,很清楚 sc
值增加或减少,都是CAS 操作。
int rs = resizeStamp(n); // n 是数组长度,
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
private static int RESIZE_STAMP_BITS = 16;
resizeStamp(n)
是计算 rs
的。
Integer.numberOfLeadingZeros(n)
这个方法指,简单说,就是这个二进制数前面几个 0。
比如 2 的二进制是 10,前面应该是 30 个0,那这个方法就返回 30。
4 的二进制是 100,前面应该是 29 个 0,那这个方法返回 29。
1 << (RESIZE_STAMP_BITS - 1)
这个相当于 1 左移 15 位。高16位是0,拼 1 个1,再拼 15 个0。
我以n = 16 为例,画了图,图好解释。
可以很清楚的看到,在扩容时,sizeCtl
这个参数,一定是负数。
高16位,保存了扩容前数组的容量信息,
低16位,保存了扩容的线程数量,(低16位若是 n,则 n -1 即为扩容的线程数)
为什么不从1开始计数呢?
因为数组初始化时,sizeCtl
设置为 -1,那个位置被占了。
至此,触发扩容最基本的流程,讲了一遍。下面讲 addCount(1L, binCount);
方法中这段
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
当一个参与扩容的线程,干完自己的活儿。一看,还需要扩容,就会执行这段。
while 循环中,会重新计算 sc 的值,小于0,就是扩容还在进行。
会调用 U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)
,调用成功,就参与扩容。
直到扩容完成,或是扩容线程数达到了最大值,跳出循环结束。
非竞争状态下,addCount() 方法就讲到这儿,竞争状态下,先不讲,后面再讲。
四、扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
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;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
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;
}
else if (U.compareAndSwapInt
(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 = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
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;
}
}
}
}
}
}
代码很长,也很不好理解。先不解析源码,先要明白以下几点。
原数组长度 n,新数组长度为 2n。扩容时,每次转移固定的节点数(stride)。
比如上图,当前线程,转移红色的节点。
若出现并发,并发线程转移灰色的节点。
即从末尾开始转移,每次固定的步长(长度 stride);
- 当本次
transfer()
结束后,调用方会决定是否再次调用transfer()
。 - 并发扩容时,每个线程,转移各自的节点(长度 stride)。
- 多个 stride,从旧数组末尾开始算,不重不漏。
- CAS操作
transferIndex
,用于并发状态下,控制不同线程的。 - 具体节点从旧数组,转移到新数组,代码与HashMap相似。
好,看过这几条之后,开始一点一点解析源码。
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
private static final int MIN_TRANSFER_STRIDE = 16;
这个是算步长的,和CPU数有关。步长最小值是 16。
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;
transferIndex = n;
}
这段和上面讲过的,启动扩容的源码,transfer(tab, null);
传入的新数组是 null。
在创建新数组时,会设置 transferIndex = n
。
/**
* The next table index (plus one) to split while resizing.
*/
private transient volatile int transferIndex;
当出现并发扩容时,这个全局变量,是用来给各个线程分配节点的。
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; // 是否结束 while 循环
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) { }
这里 for 循环做了两件事,先是计算需要转移的节点,再是将每个节点的数据进行转移。
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
……
else if ((nextIndex = transferIndex) <= 0) {
……
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
这里的三个分支,最后一个是计算需要转移的节点。
nextBound = (nextIndex > stride ? nextIndex - stride : 0)
这行不难理解。
比如原数组长度是 64,那新数组长度是 128。transferIndex
的初始值是 64。
stride 是 16,那计算出 bound = nextBound = 48, i = 63
U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound )
执行之后,transferIndex
被 CAS 操作为 48。
那本次转移的节点,就是下标 48 到下标题 63 之间的数据。
如果并发扩容,别的线程进来了,会以新的transferIndex = 48
来计算,需要转移哪些数据。
以上分析了并发扩容时,对不同线程处理节点的控制。
Doug Lea
真的是牛,不用锁,仅用 CAS 控制一个全局变量,
就很巧妙的控制了并发扩容问题。
前面说过,for 循环做了两件事,先是计算本次需要转移的节点,
再是将每个节点的数据进行转移。现在讲数据迁移的问题。
接着上面的例子,计算出 i = 63
if (i < 0 || i >= n || i + n >= nextn) { // 结束扩容的,先不用管
……
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
……
setTabAt(tab, i, fwd); // 将旧数组该节点处 hash 设置为 MOVED
advance = true;
}
}
f = tabAt(tab, i)
这是取出该节点,判断这个节点,有三种情况,
- 节点为 null (即没有数据要迁移)
- hash 值为 -1,会再下一次for循环中,开启while循环
- 其它情况 进行数据迁移,将旧数组设置一个特殊节点 fwd
假设 i = 63 这个节点是 null,属于第一种情况,
那就执行 advance = casTabAt(tab, i, null, fwd);
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
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;
}
}
fwd 是一个空节点,只是 hash 值是 MOVED( 即-1 )。
执行 advance = casTabAt(tab, i, null, fwd);
,这是CAS 操作,
如果失败,结束本次 for
循环,下次 for
循环还会执行这行,
如果成功,advance = true
,结束本次 for
循环,下次 for
循环时,
开启 while
循环, while (advance) {}
。
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
……
}
else if ( ) {
……
}
}
进入 while
循环后,执行 --i >= bound
这行,i 从 63 变为 62。相当于处理下个节点。
也就是说,在上个循环中,处理 i = 63
时,会有三种情况,现在完善下,就是
- 节点为 null ,设置一个 MOVED 节点,结束本次循环,在下次循环时,下标会减1。
- 节点的 hash 值为 MOVED,说明本节点已处理过,结束本次循环,在下次循环时,下标减1。
- 其它情况 进行数据迁移,即头节点加锁,迁移数据,完成迁移后,设置hash 值为 MOVED,结束本次循环,下次循环时,下标减1。
总之会将节点设置为 MOVED,并将下标减小 1 。
、
那什么时候,结束数据迁移呢?
还是以上面的例子,转移下标 48 到 63 的数据。
当这几个节点都转移完成了。即 i = 48
会再次进入 while
循环
bound = nextBound = 48
这两个参数,最开始计算过,是 48。
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (--i >= bound || finishing)
这个不满足,
else if ((nextIndex = transferIndex) <= 0)
这个也不满足,
那就进入第三个分支,重新计算下标,设置 transferIndex
这个全局变量。
就比如说,某线程是转移 红色的那几个节点,
转移完了,灰色的几个节点分给其它线程了,
那这个线程再分配时,可能就分配到绿色的那几个节点。
当所有节点都分配完了,那就该结束 transfer
这个方法了。
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
整体代码的思路是:
多个线程在扩容,当某线程完成自己的扩容任务时,退出,
最后一个扩容线程,将 finishing 设置为 true。
然后将旧数组检查一遍(即再次走一遍扩容代码),
确实是所有节点都已完成迁移,设置相关参数,结束扩容。
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
这行之前说过,
某线程完成分给它的迁移任务,退出时,记录扩容 线程的数量要减小1。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
这行是不是有点懵。
在讲 addCount() 触发扩容时,讲了这段
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)){
transfer(tab, null);
}
触发扩容的那个线程,设置 sizeCtl
的值是 rs << RESIZE_STAMP_SHIFT) + 2
之后若有线程加入扩容,sizeCtl
的值 加 1,有线程退出扩容,这个值 减 1。
那 if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
它的意思是,当前扩容线程数量不是1。
如果当前扩容线程数量是 1,
那应该是 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
。
当前扩容线程数量是 1 时,会设置 finishing = advance = true;
这会开启 while 循环。另外设置 i = n;
这会从旧数组最后一个节点开始,重新执行扩容代码,
检查所有的节点 hash 值是否等于 MOVED。
检查完毕,执行下面的代码
if (finishing) {
nextTable = null; // 参数置空
table = nextTab; // table 使用新数组
sizeCtl = (n << 1) - (n >>> 1); // sizeCtl 设置为当前数组大小的 0.75 倍,做为下次扩容的阈值。
return;
}
头节点加锁,进行数据迁移,这个与 HashMap 的代码雷同。
这里不再分析,不清楚的可以参考《HashMap 源码解析》。
至此,整个扩容的源码流程分析完了。
五、协助扩容
在讲 put() 方法时,说过,当插入元素时,遇到扩容,会协助扩容。
协助扩容的方法是 helpTransfer(tab, f);
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
整个代码并不复杂,其中并发控制的非常好。
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
……
}
这个 if
判断,比较好理解 f instanceof ForwardingNode
正在扩容的意思。
int rs = resizeStamp(tab.length);
这个前面讲过,也画过图,是记录扩容状态的。
int rs = resizeStamp(tab.length);
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 要么扩容结束了,跳出循环
// 要么扩容没结束,成功调用了扩容方法,跳出循环
// 扩容没结束,也没有调用扩容方法,进入下一次循环
}
(sc = sizeCtl) < 0
这个条件说明,正在扩容,前面讲过。
nextTab == nextTable && table == tab
个人认为,这个防止 扩容里套用扩容。
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
这个意思是扩容结束了,跳出while 循环。
sc >>> RESIZE_STAMP_SHIFT) != rs
这个看下上面的图,
sc 的高 16 位,与 rs 的低 16 位,应该是相等的。
不相等,那就是标识符被改动了。
sc == rs + 1
左边是负数,右边是正数,这个永远是 flase, JDK 8 的 bug
sc == rs + MAX_RESIZERS
这也是bug,同上。
它是想说明 扩容线程达到最大值了。
sizeCtl
的 低 16 位记录扩容的线程数,当低16位占满了。
说明有6万多个线程在扩容,再也不能多了。
transferIndex <= 0
这个也好解释,参与扩容的线程都会分若干个结点扩容。
拿上面的例子来讲,第一个触发扩容的线程,
先转移 下标 48 - 63 的节点,此时 transferIndex
从 64 变为 48,
再来一个线程,那就转移 下标为 32 - 47 的节点,此时 transferIndex
从48 变为 32
当 transferIndex <= 0
那就是所有节点都分出去了,不需要再协助扩容了。
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
这段就容易理解,扩容前,将 sizeCtl
加 1,表示多了一个线程参与扩容,
执行完 transfer()
,要么是扩容线程数减 1,
要么是扩容完全结束了,sizeCtl
新数组长度的 0.75倍,作为下次扩容的阈值。
六、查找元素
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
(e = tabAt(tab, (n - 1) & h)) != null
这句,是定位到某个下标,该下标处不为 null 。
这里顺便总结一个:
- 正常节点 hash 值 大于等于 0
- hash 值 等于 -1,说明正在进行扩容
- hash 值 等于 -2,说明是一个红黑树节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
这段很好理解,头节点 hash 值符合,若 key 那就说明找到了,直接返回。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
这段代码虽然很短,但是有没有疑问呢?
前面我说的很清楚、扩容与红黑树的节点,都会是 hash < 0。
难道这两种情况,走的同样的代码?
不是这样的,注释写的很清楚 overridden in subclasses.
当hash = -1,时,节点就是 ForwardingNode
,这个内部类会重写 find
方法。
当 hash = -2 时,节点就是 TreeBin
,这个内部类也会重写 find
方法。
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<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
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;
}
}
}
}
这个代码不难理解,只是我水平有限,有些地方还是没想清楚,不细说了。
红黑树的代码我不会,同样也不讲。
加过头,继续说 get 方法的最后一段
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
走到这里,说明是一个正常状态下的链表,遍历这个链表即可。
纵观整个 get 方法,没有用锁,也没有CAS。会不会 脏读呢?
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
……
}
首先,Node 类中,相关属性是用 volatile
修饰,能保证线程的可见性。
另外, transient volatile Node<K,V>[] table;
能保证扩容时,设置了新数组,
其它线程不会去读旧值。
也就是说, get
方法通过 volatile
避免了脏读。
.
七、删除元素
public V remove(Object key) {
return replaceNode(key, null, null);
}
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
虽然方法很长,其实逻辑还是很简单的。
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 删除元素
synchronized (f){...}
}
}
return null;
总体是 for 循环里三个分支,
第一个分支 :数组不存在,或该下标处为空,跳出 循环返回 null 。
第二个分支:删除元素时,遇到 扩容。那参与扩容,完成后本次循环结束,进入下一次循环。
第三个分支:加锁删除元素,hash > 0,说明是链表节点,hash < 0,说明是红黑树节点。
只说链表节点删除,红黑树的我不会哈。
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
大概逻辑就是,遍历该下标处所有元素,找到对应的 key,返回 其 value,删除此节点。
for (Node<K,V> e = f, pred = null;;) {
K ek;
if () {
// 找到对应的key
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
这是一个循环,会将此下标处的元素遍历一遍。
e 指当前节点, pred 指 e 的父亲节点。
e.next 不等于 null,会遍历下该节点的下一个节点。
如果找到,两种处理方式,当前节点是头节点,当前节点存在父节点。
分别做下面的处理。
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
最终,oldValue 不是 null,会修改键值对的数量。
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
需要说明一点,在同一下标处,删除元素与插入元素,是存在竞争的。
锁的是同一个对象,可认为是竞争同一把锁。不能同时进行。
但不同下标处,删除与插入,彼此无影响,锁资源不同。
ConCurrentHashMap
锁粒度是很细的。
在保证安全的前提下,尽可能的提高了性能。
八、计数
所谓的计数,指的是 ConCurrentHashMap
存了多少个 键值对。
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
比如调用 size 方法,返回键值对的个数。这里是把 long 型,强转为 int。
当然还有更为精确的方法,mappingCount
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // ignore transient negative values
}
这两个方法,都是调用了 sumCount()
方法。
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;
}
代码很清晰,比较容易理解,计数来自两部分,一个是 baseCount
,
另一个是 各个 CounterCell
的和。
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
private transient volatile long baseCount;
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
private transient volatile CounterCell[] counterCells;
/**
* A padded cell for distributing counts. Adapted from LongAdder
* and Striped64. See their internal docs for explanation.
*/
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
源码中注释写的很清楚,counterCells 大小是 2 的 n 次方。
CounterCell 这个内部类中的 成员变量只有一个,且是用 vllatile 修饰的。
@sun.misc.Contended 这个可以解决**伪共享
**的问题,本文不再展开讲。
以上代码很容易理解,计数方法也没有要讲的。
这里,重点说说,counterCells 是如何初始化的,它的工作原理
以及在多线程环境下,是如何计数的。
重点说明:
先明确一点,计数时,要么修改了 baseCount
,要么 修改了 CounterCell
对象中 value
的值。
前面讲触发扩容的时候,说了 addCount(1L, binCount);
这个方法。
另外在讲删除元素 remove
方法时,也调用了 addCount(-1L, -1);
这个方法。
这个方法其中一个重要功能,就是计数。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
// 可能触发扩容,前面讲过了,跟计数无关系,这里省略
}
}
假设 counterCells
还没有初始化,现在有 4 个线程,同时执行为个方法,
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x))
(as = counterCells) != null
,按假设来说,这个返回 false,看下一个判断。
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
四个线程同时执行这行,那只有一个线程会执行成功,
将 baseCount
修改,不进入方法体。
其它三个线程执行方法体中的方法。
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
按假设来讲,as == null
返回 true,三个线程都会执行 fullAddCount(x, uncontended);
。
这个方法就是进行精确的计数的。等会再细讲。
现在假设 counterCells
已经初始化,且 size 大于0。
还是 4 个线程同时执行 addCount
方法。
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x))
(as = counterCells) != null
,按假设来说,这个返回 true,
后半个判断就不会执行了,四个线程都进入方法体。
四个线程都会执行 (a = as[ThreadLocalRandom.getProbe() & m]) == null
ThreadLocalRandom.getProbe() 这个方法是返回一个随机数,彼此之间不同。
as[ThreadLocalRandom.getProbe() & m]
这个是得到数组中的一个元素。
求下标的公式和 HashMap
的一样,这里也不展开来说。
每个线程获取的随机数是不一样的,各自算出一个下标。
假设极端情况下,4 个线程计算出的下标是同一个。
若该下标处元素为 null,那 4 个线程,都会执行 fullAddCount(x, uncontended);
。
若该下标处元素不为null,那 4 个线程都执行
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
那有一个线程成功修改 counterCell 中 value 的值,完成这个线程的计数,返回。
剩下的3个线程,都会执行 fullAddCount(x, uncontended);
。
总结下:
执行 addCount 时计数时,
若 counterCells
这个数组未初始化, 非竞争条件下,修改 baseCount
,否则执行 fullAddCount(x, uncontended);
若 counterCells
这个数组已经初始化, 非竞争条件下,修改 对应的 counterCell
,否则执行 fullAddCount(x, uncontended);
如果上面的都清楚了,咱就开始分析 fullAddCount(x, uncontended);
方法
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
代码看起来很长,但分解开来,没有特别难的。
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
这段是为了让线程获取随机数,先让其初始化。
具体细节就不展开讲了,有兴趣的,可以看下 ThreadLocalRandom
源码。
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
// 修改某个CounterCell,有可能会对 counterCells 进行扩容
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// 初始化 counterCells
}
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
// 修改 baseCount 的值
}
这整体是一个无限循环,三个分支执行成功一个,就可以跳出循环。
否则一直执行。最终的结果是 要么修改了**baseCount
** ,要么 修改了 CounterCell
先看第二个分支,初始化 counterCells
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0){
……
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
cellsBusy == 0 && counterCells == as
这两个判断是并发的控制。
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)
这个是CAS修改 cellsBusy
,
cellsBusy
是 1 指在初始化或是在操作 CounterCells
。
执行成功的那个线程,初始化**counterCells
**
/**
* Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
*/
private transient volatile int cellsBusy;
然后看方法体
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
**CounterCell
**的初始容量是2,并且把 new 一个 CounterCell
对象,记录了 数值
初始化完毕后,将 cellsBusy
设置为 0;
再看第一个分支
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // 当前没有线程操作数组
CounterCell r = new CounterCell(x); // new 一个对象出来
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { // CAS 设置 cellsBusy 为1
boolean created = false; // 开关
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) { // 确保数组存在,对应下标处为空
rs[j] = r; // 对象放数组里
created = true; // 开关
}
} finally {
cellsBusy = 0; // 操作完后,一定将 cellsBusy 改回 0
}
if (created)
break; // 设置到成功的情况下,退出。
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break; // 对应下标处不为空,且CAS 记录数据成功,退出
else if (counterCells != as || n >= NCPU) // 当数组size 大于CPU 数量,不让数组扩容。
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try { // 执行扩容
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
if ((a = as[(n - 1) & h]) == null)
意思是 某下标处元素为空,这个小分支,
总的逻辑就是,线程安全情况下, new 一个 CounterCell
对象,记录了数值,设置到数组中。
这个应该好理解,我把注释写代码里了。
第二个大分支就是想办法设置 CounterCell
,必要情况下扩容数组。
第三个大分支更好理解,修改 baseCount
。
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break;
fullAddCount 方法 代码虽然很长,但各分支逻辑很清晰。
就是:在充分控制并发的情况下,修改 CounterCell 或是 baseCount。
修改不成功,就重试,直到成功为止。
九、总结
本篇文章,在HashMap的基础上,分析了 ConcurrentHashMap 是如何控制并发的。
- 触发扩容、扩容、协助扩容、计数等等操作,大量用了CAS 操作,
- 在具体删除、添加节点操作,用了 synchronized 锁,
- 扩容时,专门有 ForwardingNode 对象,transferIndex、sizeCtl属性来控制
源码很复杂,也很精妙,本人水平有限,只分析了部分常用的方法。
有分析不到位或是错误的地方,烦请批评指正。
若是对解析的某些方法,有疑问,欢迎在留言区讨论。