目录
一、Map的初始化
map初始化的方法为initTable(),使用CAS方法保证只有一个线程来对map初始化。最外层的while循环判断map是否仍未空,不为空说明已经有线程完成初始化了,直接返回。
之后再根据sizeCtl的值判断当前是否有线程正在初始化。sizeCtl是一个成员变量,可能出现三种情况:
- > 0,说明map没有正在初始化或扩容,这时的值表示map的容量阈值
- == -1,说明map正在进行初始化
- < -1,说明map正在进行扩容,值为一个很小的负数,可以计算出有几个线程正在帮助扩容
线程根据sizeCtl的值决定有没有并发冲突,使用CAS将sizeCtl值为1表示自己获得了控制权(相当于获得了锁,但开销比悲观锁小)。
/**
* 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;
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
// 让这个线程对map进行初始化,将sc置为-1
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;
}
compareAndSwapInt()方法的四个参数为:
- 要改变的对象
- 要改变的字段在第一个参数中的偏移量
- 期待看到的值(比较的值、修改前的值)
- 要交换的值(修改后的值)
根据调用时传入的参数可知,首先判断要sizeCtl的值与之前一样,保证在这期间没有线程开始初始化。之后将sizeCtl的值置为-1,让自己获得初始化的控制权。以上这一比较与交换过程由操作系统保证原子性。SIZECTL时sizeCtl属性在ConcurrentHashMap中的偏移量,这是Unsafe对象U算出来的。
二、插入数据
在putVal()方法的执行流程为:
- 判断当前hashmap是否为空,为空则先去初始化
- 判断要插入的bin是否为空,为空则使用CAS插入数据
- 判断hashmap是否正在扩容(bin的首节点的hash值为-1),若正在扩容则取帮助扩容
- 对这个bin加锁,完成插入操作
其中在对于bin加锁之后,不存在线程安全的问题,因此只需考虑casTabAt()、initTable()、helpTransfer()、addCount()四个方法的并发设计
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key和value不许为null,与HashTable一样,与HashMap不一样
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;
// 情况1:初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 情况2:使用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
}
// 情况3:帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 情况4:加锁插入
synchronized (f) {
// ... 插入数据 ...
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 最后:binCount + 1
addCount(1L, binCount);
return null;
}
casTabAt()方法中只有一条语句,与初始化时设置sizeCtl相似,也是使用CAS的方法保证线程安全地插入数据
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);
}
三、元素计数
ConcurrentHashMap中没有直接维护size变量来记录当前map中存储的元素数量,而是存储了一个CounterCell数组,名为CounterCells,以及一个baseCount变量。每一个CounterCell对象中存储一个数值,需要计算总元素个数时,使用sumCount()方法将所有的CounterCell中的数值和baseCount的值求和。这样做的优点是出现并发操作之前,直接将baseCount+1就可以了,有并了发操作后,每个线程可以互不影响地单独修改一个CounterCell的值(用过CounterCell后就不再用baseCount了,但有一种情况除外)。
如何把一个线程分配到一个CounterCell?使用ThreadLocalRandom.getProbe()获得线程随机数,对CouterCells的长度取模。
/**
* 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; }
}
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;
}
在putVal()方法的最后,调用了addCount()方法将元素数量+1,addCount()方法的执行流程为:
- 判断是要修改baseCount还是CounterCell。如果目前都没有出现过并发,且CAS能直接修改baseCount,则直接修改
- 如果需要修改CounterCell,使用CAS修改CounterCell,对于以下几种特殊情况,调用fullAddCount()方法继续完成操作:(1)CounterCells为空;(2)要修改的CounterCells中的元素为空;(3)CAS修改CounterCell失败
- 检查是否需要对map扩容
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) {
// 判断是否需要扩容
}
}
fullAddCount()方法在上述几种特殊情况下保证继续对CounterCell完成操作,对于上面说的第一种情况,CounterCells数组为空,使用CAS尝试获取锁,若能获取到,当前线程完成对数组的初始化。
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;
}
若不能获取到,说明当前有其它线程正在初始化CounterCells数组,这个线程也不等了,直接使用CAS修改操作baseCount。
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
对于上面的第二种情况,要修改的CounterCell对象为空,也是使用CAS获取锁,初始化这个位置上的CounterCell对象。
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
// 先创建一个CounterCell对象
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;
}
// 其它情况
.....
四、Map的扩容
ConcurrentHashMap的扩容可以由多个线程共同完成,每个线程负责迁移map中的一部分(负责范围 = stride)。扩容的过程从后向前,每个线程使用变量 i 记录当前准备扩容的bin,使用bound表示目前需要负责的最小bin的下标。当一个线程已经将自己要负责的部分全部迁移完成后,会继续从transferIndex开始,向前认领另一分部(范围仍然 = stride)。可见,transferIndex会被多个线程修改,因此对于transferIndex的修改使用CAS完成。
在for循环的开始部分,当前线程先要确认自己这次要迁移的bin的下标和自己的bound(while循环),advance变量表示当前线程是否完成了对bin[i]的迁移,即是否需要向前移动。当自己当前的任务已经完成时,可能要去认领新的任务。
ForwardingNode是一个静态内部类,一个bin的位置完成迁移后,将这个bin的第一个节点赋值为ForwardingNode类型,用来标识已经完成迁移的部分
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 计算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;
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;
}
// 从transferIndex开始向前认领stride部分的扩容任务,使用CAS修改transferIndex
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
根据刚刚确定的 i 的值,判断是否已经完成扩容。
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 更新数组的引用
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 使用CAS修改sizeCtl的值 - 1
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
}
}
若bin[i]的位置为空,说明不用迁移,直接将这个位置的节点设置为ForwardingNode表示完成迁移。若不为空,则对bin[i]加锁,完成数据的迁移。
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) {
// 。。。
}
}
// 。。。