我们知道ConcurrentHashMap是与HashMap相比其是线程安全的,其的原理主要是通过CAS来对对应变量进行修改,同时synchronized
来对并发时的一些主要逻辑进行锁操作,其锁的对象是数组对应槽位的第一个元素(也就是锁住了对应槽位的整条链表),下面我们就来具体分析下其实现线程安全的原理。梳理一些主要的逻辑内容,建议在阅读本篇前阅读下这个系列的第8篇关于HashMap的实现,对Map结构的存储逻辑有个整体的介绍,这篇主要是梳理下重点方法对并发的处理。
一、结构
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
可以看到其继承于类AbstractMap,同时实现ConcurrentMap接口(这个接口相对来说并没有特别的)。
二、构造方法
public ConcurrentHashMap() {
}
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
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;
}
这里可以看到就是对扩容阈值sizeCtl
的初始化计算,同时上面的tableSizeFor
方法我们在HashMap中也有梳理,就是用来计算进位的。
三、变量
1、MAXIMUM_CAPACITY
private static final int MAXIMUM_CAPACITY = 1 << 30;
最大的容量。
2、DEFAULT_CAPACITY
private static final int DEFAULT_CAPACITY = 16;
默认的容量。
3、MAX_ARRAY_SIZE
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
最大的数组长度。
4、DEFAULT_CONCURRENCY_LEVEL
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
默认的并发级别,现在没有使用了(1.8 & +),这个好像是因为以前是使用Segment这种结构?
5、LOAD_FACTOR
private static final float LOAD_FACTOR = 0.75f;
扩容的加载因子,等同于HashMap的该字段。
6、MOVED
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
MOVED表示正在扩容。
7、NCPU
static final int NCPU = Runtime.getRuntime().availableProcessors();
当前系统的CPU的核数量。
这上面省略了一些关于树结构相关的变量。
8、table
transient volatile Node<K,V>[] table;
类似于HashMap的Node数组,不过其前面加了volatile
,来实时感知其的变化。
9、nextTable
/**
* The next table to use; non-null only while resizing.
*/
private transient volatile Node<K,V>[] nextTable;
下一个数组,这个是用来处理并发扩容的。
10、baseCount
private transient volatile long baseCount;
用来对当前Map放了多少元素进行计数。
11、sizeCtl
private transient volatile int sizeCtl;
对数组扩容的控制,达到该值的元素容量就进行扩容。同时这个字段还有其他的意思。当小于0的时候:如果为-1
表明其正在进行扩容,其他更小的是( 1 + 正在调整线程扩容的线程数量),但好像JDK源码的注释是有问题的(或者是翻译的问题?),这个其实要将其往后移动16位才是其要表达的意思。
12、transferIndex
private transient volatile int transferIndex;
我们知道ConcurrentHashMap是用来处理并发的,所以在扩容的时候其也是使用分段,用多线程(如果存在并发的话)来将对应段的数据转移到新数组中(其是从后往前的),这个transferIndex表明下一个线程要扩容的话是从哪个位置开始。
13、cellsBusy
/**
* Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
*/
private transient volatile int cellsBusy;
这个是在扩容用来自旋操的。
14、CounterCell
private transient volatile CounterCell[] counterCells;
@jdk.internal.vm.annotation.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
这是用来看那些扩容计算是通过多线程处理的。这里的@jdk.internal.vm.annotation.Contended
,是一个缓存行的概念,可以搜索其他的博文了解。value就是用来计算使用的。
15、MAX_RESIZERS
/**
* The maximum number of threads that can help resize.
* Must fit in 32 - RESIZE_STAMP_BITS bits.
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
能帮助扩容的最大的线程数量。
16、RESIZE_STAMP_BITS&RESIZE_STAMP_SHIFT
private static final int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
这两个其值都是16,但其使用的场景不同。这个需要结合sizeCtl
来梳理。RESIZE_STAMP_BITS
是左移、RESIZE_STAMP_SHIFT
是右移。
17、CAS相关操作的变量
private static final Unsafe U = Unsafe.getUnsafe();
private static final long SIZECTL;
private static final long TRANSFERINDEX;
private static final long BASECOUNT;
private static final long CELLSBUSY;
private static final long CELLVALUE;
private static final int ABASE;
private static final int ASHIFT;
static {
try {
SIZECTL = U.objectFieldOffset
(ConcurrentHashMap.class.getDeclaredField("sizeCtl"));
TRANSFERINDEX = U.objectFieldOffset
(ConcurrentHashMap.class.getDeclaredField("transferIndex"));
BASECOUNT = U.objectFieldOffset
(ConcurrentHashMap.class.getDeclaredField("baseCount"));
CELLSBUSY = U.objectFieldOffset
(ConcurrentHashMap.class.getDeclaredField("cellsBusy"));
CELLVALUE = U.objectFieldOffset
(CounterCell.class.getDeclaredField("value"));
ABASE = U.arrayBaseOffset(Node[].class);
int scale = U.arrayIndexScale(Node[].class);
if ((scale & (scale - 1)) != 0)
throw new Error("array index scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
// Reduce the risk of rare disastrous classloading in first call to
// LockSupport.park: https://bugs.openjdk.java.net/browse/JDK-8074773
Class<?> ensureLoaded = LockSupport.class;
}
可以看到这里是通过Unsafe类来进行CAS原子操作的。
四、内部类
1、Node
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(int hash, K key, V val) {
this.hash = hash;
this.key = key;
this.val = val;
}
这个其实与HashMap的结构以及变量定义,不过这里不同的是val
、next
前面加了valatile
关键词来处理并发,如果有并发就能实时感知这个值的变化。
2、ForwardingNode
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null);
this.nextTable = tab;
}
...
Node(int hash, K key, V val) {
this.hash = hash;
this.key = key;
this.val = val;
}
将要扩容那段数组构建为ForwardingNode,,可以看到其的key是设置的MOVED
的。
五、主要方法
1、tabAt(Node<K,V>[] tab, int i)
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);
}
获取tab
的i
位置的值。
2、casTabAt(…)
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
使用CAS来设置tab
对应i
位置的值,如果是预期值c
就将其设置为v
。
3、setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectRelease(tab, ((long)i << ASHIFT) + ABASE, v);
}
这个方法也是在指定位置i
设置对应v
的值,不过其没有比较再设置(看逻辑,对其的调用都是在synchronized中)。
4、spread(int h)
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
再计算h
的hash值。
5、putVal(K key, V value, boolean onlyIfAbsent)
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; K fk; V fv;
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)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent && fh == hash && // check first node
((fk = f.key) == key || fk != null && key.equals(fk)) &&
(fv = f.val) != null)
return fv;
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);
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;
}
}
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);
return null;
}
这个就是其的put方法,可以看到ConcurrentHashMap是不能添加key&value为null
的元素,现在我们来具体分析其的逻辑(省略关于红黑树的处理逻辑)。
1)、首先是通过spread(key.hashCode())
方法对key再计算一次对应的Hash值。
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
2)、binCount = 0;
这个变量是用来记录当前槽位的链表的长度的。
3)、然后再遍历Node<K,V>[] tab = table;;
,可以看到这个for循环的添加部分与自增部分是没有值的,就表名其是一种等同于自旋的操作,依赖内部的return
、break
这样的跳出逻辑。
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
这里的fh
、fk
、fv
指的是前面对应的f
局部变量的hash、key、value值。
4)、首先如果table
还没有进行初始化,就通过initTable
方法对其进行初始化。
if (tab == null || (n = tab.length) == 0)
tab = initTable();
5)、首先是判断在table
的tab, i = (n - 1) & hash)
位置是否已经有数组值了,如果没有,就通过casTabAt
方法去设置,可以看到其的预期值是null
,如果返回true
就表明其是成功的,就能通过break
跳出逻辑。
这这个地方涉及到对成员变量table
的修改,所以我们要考虑到并发。这里我们在前面有提过volatile Node<K,V>[] table
是有加valitile
关键字的,但valitile
是可见性以及禁止重排序,而并没有原子操作。这里在设置定义i
位置的值的时候,是先判断有没有值(并发可能其它线程已经添加了),再进行对应的赋值,这是两个操作,valitile
关键字并没有原子性,所以就需要使用casTabAt
来进行原子操作赋值,(如果这个时候其他线程已经将该位置赋值了,此线程比其他线程慢了,所以此线程的这个节点就需要添加在那个节点的下一个节点)。如果赋值失败,则进行下一次循环。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
6)、如果这个节点的hash
值是MOVED
(通过第5)进行的赋值(表明数组该位置已经有值了,这个时候获取的是链表的第一个节点),前面提过,表明正在进行扩容,就加入helpTransfer
来帮助其他线程一起来扩容。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
7)、然后如果该位置有值了,并且也没有进行扩容,就判断这个位置的节点与要添加的节点是否相等并通过onlyIfAbsent
判断是否只在不存在的时候才进行添加,如果是,就这个返回这个已经存在节点的原来的值。
else if (onlyIfAbsent && fh == hash && // check first node
((fk = f.key) == key || fk != null && key.equals(fk)) &&
(fv = f.val) != null)
return fv;
8)、当上面都不满足了,就加入正式的添加逻辑。在这里就使用了synchronized
关键字来加锁,其锁住的是f
,table
数组该位置的节点**(链表的第一个添加的节点,然后每次for循环的实时都是获取的数组index位置,所以这样就锁住了整个链表)**,通过前面一系列的判断来尽量缩小锁的颗粒。
V oldVal = null;
synchronized (f) {
9)、再通过CAS获取一次tab数组i位置的值是不是我们原来获取的第一个元素(这个线程已经上锁了),因为可能其他线程再我们上锁前就将其改变了,例如将这个节点删除了或者改变位置了。在判断节点f
的hash值还是不是>=0
,因为如果扩容这个hash值是会被修改为MOVEN
-1。
if (tabAt(tab, i) == f) {
if (fh >= 0) {
10)、对binCount自增,也起到记录当前位置链表的节点个数。找到当前位置链表的最后一个位置,然后进行赋值pred.next = new Node<K,V>(hash, key, value);
逻辑。
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);
break;
}
}
}
11)、下面关于TreeBin的逻辑我们目前跳过。
12)、前面是将这个put的元素添加到对应数组+链表的对应位置。但我们还要处理扩容逻辑&确保添加的这个元素能记录到统计添加元素个数的成员变量中。
addCount(1L, binCount);
return null;
下面我们就来具体分下上面关于添加元素方法的其他调用方法。
6、initTable()
/**
* Initializes table, using the size recorded in sizeCtl.
*/
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.compareAndSetInt(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;
}
这个就是其的扩容方法。可以看到:
1)、首先是判断当前table
有没有进行初始化创建赋值,没有的话才进入初始化逻辑。这里的初始化与HashMap也是有部分不同,ConcurrentHashMap的初始化&扩容是完全独立的两个方法。
2)、再判断(sc = sizeCtl) < 0
是否小于0,因为可能其他的线程也在进行初始化创建。如果竞争失败,就通过Thread.yield()
方法让出CPU,等待CPU的下次调度,下次的时候,再进行while循环(tab = table) == null
,如果其他的线程初始化创建成功了,就不进行下面逻辑,直接return tab;
。
3)、如果当前线程通过CAS竞争成功,将SIZECTL
修改为了-1
,其就会在上一步进入yield
状态。下面就是当前线程的初始化数组状态。
4)、同时在里面也进行了一次((tab = table) == null
检查。双重检查,类似于单例模式的创建对象。这里我刚才还在想一种转态,就是如果其他线程已经完成了初始化了,刚好完成下面的finally里面的sizeCtl = sc;
,这个时候sizeCtl
就不为-1
,并且这个时候刚好当前线程走到if ((sc = sizeCtl) < 0)
这里就会不成功,然后在CAS也能成功进入,会存在重复创建的内容。由于双重检查以及table
是有volatile
修饰的,就解决了重复创建的问题。同时我们再进行单例创建的时候也可以借鉴这里的这种写法。
5)、这里对数组的初始化的长度是看构造方法对sizeCtl
的赋值情况,如果没有就算DEFAULT_CAPACITY
,然后就是扩容阈值的赋值了,sc = n - (n >>> 2);
、sizeCtl = sc;
。可以看到这个sizeCtl
是有多个含义,并不像HashMap的阈值那样只表明扩容阈值这一个含义。
6、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;
}
这个方法就是用来计算当前Map总的元素个数,可以看到其的计算是baseCount
与counterCells
数组的value和。
7、addCount(long x, int check)
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSetLong(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.compareAndSetLong(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.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSetInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
这个方法主要是有两个逻辑,计算元素的个数&扩容。同时这个入参对于putVal
方法来说,其传入的是对应链表的元素个数,所以其是>0的。
1)、这里最开始的逻辑是判断counterCells
,为不为空。如果为空,就表示还没有发生线程之间的竞争。有没有竞争再是通过后面的!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)
,判断的,如果设置失败,就表示有竞争。如果没有竞争失败,就通过CAS对baseCount
成员变量进行进行设值也就是+1,对于putVal
方法addCount(1L, binCount);
。
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
2)、如果竞争失败了(这个数量就没有计算到,就会将这个数据计算设置到CounterCell
中)。这里首先是判断as
也就是counterCells
有没有被初始化,没有或者当前线程对应的CounterCell
为空,这个ThreadLocalRandom.getProbe()
与线程相关的一个值,可以简单理解为其是与线程相关的一个固定的hash
值,如果没有对其进行初始化,其的默认值为0
。
再或者已经存在当前线程对应的CounterCell
了,就对其的值进行+1
,也就是表示这个元素的计算是加在了当前线程对应的CounterCell
中了。所以要计算ConcurrentHashMap中的元素,是需要baseCount
+CounterCell
数组中的value和的。这个if都不满足,例如没有竞争就直接CAS对CELLVALUE
设值成功了,就会s = sumCount();
计算获取总的元素值(check>1),不然是直接return。
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.compareAndSetLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
3)、这个就是检查需不需要扩容。如果多线程的话,就可以通过别的线程来将其进行扩容。这里的逻辑了解起来有点费劲,有一个地方我目前还没有理解sc == rs + 1
,这个等式应该一直不会等(我在测试的时候也没有出现这个情况,有大佬看到 的话麻烦赐教)。下面我们就来具体分析下。
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);
......
}
}
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
首先我们需要明白sizeCtl<-1
那部分表示目前有多少个线程在帮助扩容的含义(我们前面有提过需要将其往右移16位)。
1、先看resizeStamp
方法,这个(1 << (RESIZE_STAMP_BITS - 1)
其的二进制表示就是1000000000000000
共16位其的int是32768
。同时Integer.numberOfLeadingZeros(n)
方法是看int类型32位n
其前面有多少个0,例如如果n
为16,则其前面就有27个0。所以这个方法的返回是在32768
-32768+32
之间。不过由于默认16的关系,所以其一般应该是在32768+27
之间。例如如果n为16,则这个方法返回的二进制是1000000000011011
。然后对于线程等待计数10000000000110110000000000000010
,用低16为来表示。
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
......
}
else if (U.compareAndSetInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
2、然后回到while方法,s >= (long)(sc = sizeCtl)
其中s
在前面已经算过其实当前Map中的元素,这个while中的条件就是判断需不需要进行扩容(可以是帮助扩容 ,也可以是扩容的最前面的一个)。
3、再通过int rs = resizeStamp(n);
方法对rs
进行赋值,这个局部变量的目的我的理解是:因为n表明的是tab数组的长度,所以其实rt
就是来记录判断tab
数组有没有完成扩容(扩容一般就会产生进位了)。
4、现在我们来具体看下这个逻辑。
这里假如现在没有多线程竞争,所以sc = sizeCtl
是>0的走的是下面else if
的逻辑。其对sizeCtl
的赋值逻辑是:rs << RESIZE_STAMP_SHIFT + 2
,这个其实就是将rs的值赋值到高16位(所以sizeCtl
就会<0了),同时这个+2
就相当于-2
,所以这个时候扩容的线程就是|2|-1
为1(因为sizeCtl
在前面提过,-1表MOVEN
)。然后再调用transfer(tab, null)
去扩容。
如果有其它线程竞争,就会有两种情况。在else if
竞争失败(本线程),由与其他线程现在再扩容(还未完成)。竞争失败,再次while
,由于还未完成扩容,所以s >= (long)(sc = sizeCtl)
是满足的。再进行if (sc < 0) {
判断,就会满足。下面我们就来分析这个if中的内容。
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.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
e......
}
5、这里的sc >>> RESIZE_STAMP_SHIFT
其实就是获取原来的高16位的值,也就是竞争成功还没有完成扩容的那些线程是同一个值,判读与rs
是不是相等(要理解前面写的关于rs
的描叙解析,不知道我又没有描叙明白
),如果不相等,就表明已经进位完成扩容了,就可以break
了。
这里还有剩余的4个或条件:sc == rs + 1
这个我还没有明白这两个什么时候能相等?、sc == rs + MAX_RESIZERS
就是判断有没有达到帮助扩容的最大线程、(nt = nextTable) == null
&transferIndex <= 0
表明是不是不再需要其他线程的帮助了。如果还需要就通过CAS来修改sizeCtl
表明又增加了一个线程来帮助扩容。
8、fullAddCount(long x, boolean wasUncontended)
// See LongAdder version for explanation
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.compareAndSetInt(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.compareAndSetLong(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.compareAndSetInt(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.compareAndSetInt(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.compareAndSetLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
这个方法就是对counterCells
的初始化,表明有多个线程竞争。
1)、这先是对当前线程的getProbe
的初始化ThreadLocalRandom.localInit();
,再wasUncontended = true;
表明没有竞争。
// See LongAdder version for explanation
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
2)、这里是for (;;)
,也就是依赖内部跳出。如果已经完成了(as = counterCells) != null
的初始化,就看当前线程对应记录添加数有没有初始化(a = as[(n - 1) & h]) == null
,cellsBusy
是用来在for
中自旋的。没有初始化就创建new CounterCell(x)
。
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
......
}
collide = false;
3)、在CAS将cellsBusy
修改为1
,表明有其它的正在处理(后面对cellsBusy
修改为1的时候,预期值都是为0,也就是锁住了其他修改,让其不能进入其他逻辑)。然后再再里面进行了一次判断rs[j = (m - 1) & h] == null
,再将其赋值rs[j] = r
,如果已经成功了created = true
,再将cellsBusy = 0
,复原,最后break;
,如果不满足判断条件、或者赋值失败,则continue
,下次竞争。如果竞争失败,不会进入break
或continue
,就会collide = false;
。
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSetInt(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;
4)、 如果(a = as[(n - 1) & h]) == null
不满足,表明这个线程的CounterCell
已经完成创建了。如果wasUncontended
为false
则将其wasUncontended = true;
,继续下一个循环。先一个就会进入CAS修改CounterCell
的计数了,如果竞争成功,就完成了本线程的竞争添加元素计数,然后break;
。
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
......
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSetLong(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.compareAndSetInt(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);
}
如果失败,就counterCells != as
查看其还是否相等。在else if (counterCells != as || n >= NCPU)
后面字段我的理解是如果竞争太激烈了,就进行counterCells
的扩容舒缓竞争压力。不知道有没有具体理解这里的含义,而导致认知错误?
5)、这里就是如果还没有完成对counterCells
初始化创建,则进行初始化创建,可以看到其的初始化的长度是new CounterCell[2]
。
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
......
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSetInt(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.compareAndSetLong(this, BASECOUNT, v = baseCount, v + x))
break;
9、transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
/**
* Moves and/or copies the nodes in each bin to new table. See
* above for explanation.
*/
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.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 = (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; // 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) {
......
}
}
}
}
}
}
这个方法与前面的addCount方法
一样也是比较复杂。我们可以看到相对来说其的插入元素的逻辑还是比较简单。
1)、首先是设置数据扩容的时候,分段的默认值
int n = tab.length, stride;
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;
2)、nextTab
表明数据扩容的目的数组,这里也是进行默认的初始化,可以看到其是Node<?,?>[n << 1];
,向左移动一位。同时、transferIndex
表示的是下次(例如其他线程)帮助扩容,由这个地方的槽位开始往0靠(在前面提过,其实由后往前),同时由于这个方法的进入之前的addCount
方法的逻辑,只有最开始才会nextTab
为null
,后面其他的线程进入就会有值了。
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;
}
3)、将nextTab
包裹为ForwardingNode
表示正在进行扩容,advance
表示还需不需要往前推继续扩容,finishing
表明是不是已经完成扩容了(不然进入到for循环中线程会一直再里面帮助扩容)。
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
4)、首先看这个for结构,其没有写判断条件,只能依赖里面的return
,break
,不过这里是用的return
,同时我们可以看到这个return
有一个是由finishing
控制的,同时finishing
又是由下面的CAS设置控制的。
其的进入条件首先是i < 0 || i >= n || i + n >= nextn
,这个i
表明的是数组的index,如果i
小于0(表明已经遍历完成),或i >= n
n是原来tab
数组的长度,亦或者i + n >= nextn
,nextn
是新扩容的数组的长度,满足这些条件中的一个,就进入下面的可能return
逻辑。
CAS对sizeCtl
的-1
赋值(表明可能已经都分段扩容任务完成了,这个线程已经不能再帮助扩容了,将帮助扩容的线程数量-1),如果成功则判断(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT
,注意这里的顺序:<<
优先级比!=
高,这里其实 就是有其它的线程还在继续数据转移到新数组,但当前线程已经没有新的端来让其处理了,也就会达到这个!=
的逻辑,也就当前线程能return
。
当最后一个线程完成了,这时候sc - 2
就表示已经没有线程来帮助扩容了,就能达到与resizeStamp(n) << RESIZE_STAMP_SHIFT
相等,也就会finishing = advance = true;
&i = n;
(i
为原来数组的长度),再下次for循环的时候就会if (finishing)
满足,由这个最后的线程来完成sizeCtl
,设置下次扩容的值sizeCtl = (n << 1) - (n >>> 1);
并完成nextTable = null;
。这样就借助所有的线程完成了数据转移到新的数组。
for (int i = 0, bound = 0;;) {
......
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
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; // recheck before commit
}
}
......
else {
......
}
}
}
下面我们就来看下具体是怎样分段多线程数据转移的。
5)、advance
默认为true
,这里就是找到当前数据需要负责那段的数据转移。i
、bound
默认为0
,所以最开始if (--i >= bound || finishing)
这个是不满足的。bound
指的是当前线程处理的界限(结束的位置,靠近0那端),i
表示的是本线程处理端的开始,也就是transferIndex
那边。前面我们在4)那部分分析了i
、finishing
的可能赋值,对于finishing
来说其就会进入这里来下次进入后面的for
逻辑退出。同时这个--i
,其实也完成了自减,这里的--i >= bound
就表明i
,还在这个当前线程数据转移里面。
**同时我们需要注意,对于这段代码来说,是有多个线程再处理的。这些线程的i
、bound
是不一样的,但都是划分在原来的tab
设置长度之间的。**主要是要连接不同线程处理不同段的数据转移。
nextIndex
的含义是当前线程处理的端的开始位置nextIndex = transferIndex
,如果还能分配,这个就会>0,就不会跳出,就会往下进入对TRANSFERINDEX
的修改,将其往后再移动,表明当前段被其承包了。如果已经没有更多段了,就进行i = -1;
、advance = false
,来满足退出while
以及if (i < 0 || i >= n || i + n >= nextn)
的条件来退出。
现在我们来看下TRANSFERINDEX
的CAS,这个stride
是在前面赋值的,表明每段分多长,将下次的起点赋值给transferIndex
,再将本线程负责的这段的结束赋值给bound
,即下次的起点,在将本次的起点赋值给i = nextIndex - 1
i
来进行循环遍历。
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.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
6)、这里就是CAS获取当前i
的第一个节点,如果为null
,将这个位置用fwd
表示,表明tab
的这个index正在扩容,但由于没有值,所以没有数据转移的过程。
然后判断(fh = f.hash) == MOVED
,如果满足就表明正在扩容,则advance = true;
,让当前线程去找下一段去处理。
如果上面都不满足就表明需要进行数据转移了,可以看到这里是synchronized (f)
,锁住第一个,来达到锁住整条链表,同时在前面我们分析插入元素的时候,也会synchronized (f)
,这样在数据转移的时候就不能在本条链表中插入数据了。
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) {
7)、可以看到在这里也进行了双重检查。同时这里数据的转移也涉及到低位&高位的概念,这部分就再赘叙了,可以去看下前面写的关于HashMap的分析,里面有具体分析。但这里有不同的是这里并不是用四个成员变量来按顺序对应构建高位。这里是int b = p.hash & n;
、runBit = b;
(runBit
的取值是会为0(低位,不需要+扩容的长度)或n(高位,需要+)),其表明的含义是如果上次与下次的高低位子不同就改变runBit
。也就是说,如果是101011111
这种顺序,就会到第5个位置的1停止。
这样在下面的for (Node<K,V> p = f; p != lastRun; p = p.next)
逻辑就会停止,不需要再创建新的节点来转移后面的节点,算是一种优化(需要满足后面是相同的高位或低位),但这里如果不满足在后面部分存在较多相同的高位或低位,不是多遍历了一次?(亦或者是我的理解由问题?)
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;
}
然后是通过setTabAt
方法将其赋值到扩容的数组中。并用setTabAt(tab, i, fwd)
表明当前index位置的链表正在扩容。
以上就是扩容的具体逻辑。
10、remove(Object key, Object value)
public boolean remove(Object key, Object value) {
if (key == null)
throw new NullPointerException();
return value != null && replaceNode(key, null, value) != 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) {
......对f节点链表进行遍历搜索
}
else if (f instanceof TreeBin) {
......
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
这个就是其的获取逻辑。我们可以看到主要有两点:synchronized (f)
,还有就是如果正在进行扩容else if ((fh = f.hash) == MOVED)
,就会调用helpTransfer(tab, f);
,先取帮助扩容。
至此,关于ConcurrentHashMap的主要逻辑原理就梳理了。大体的内容还是明白了,但其中还有几个小点不是很能确认其的确切含义。