以前感觉HashMap难懂,直到我看了ConcurrentHashMap。。。不过,等真的读懂了源码,不得不感叹,Doug Lea大爷还是你大爷,看的过程中,不时惊呼:原来是这样啊!这也太牛了!好了,首先介绍一下,ConcurrentHashMap是一个线程安全的HashMap,其主要采用CAS操作+synchronized锁的方式,实现线程安全,其中synchronize锁的粒度为桶中头结点(包括链表Node结点,包装红黑树的TreeBin结点),底层依然由 “数组”+链表+红黑树 的方式实现。
sizeCtl: 它是一个控制标志符,取值不同有不同的含义:
ConcurrentHashMap
容量的0.75倍,这与loadfactor
是对应的。桶中元素的hash值的含义:
HashMap
中的节点类似,只是其val
变量和next
指针都用volatile
来修饰。且不允许调用setValue
方法修改Node的value
值。这个类是后面三个类的基类。TreeNode
。但是与HashMap
不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode
放在TreeBin
对象中,由TreeBin
完成对红黑树的包装。而且TreeNode
在ConcurrentHashMap
继承自Node类,而并非HashMap
中的继承自LinkedHashMap.Entry
类,也就是说TreeNode
带有next指针,这样做的目的是方便基于TreeBin
的访问。root
的TreeNode
节点,这个是他所包装的红黑树的根节点,也就是说在实际的ConcurrentHashMap
“数组”中,存放的是TreeBin
对象,而不是TreeNode
对象,这是与HashMap
的区别。另外这个类还带有了读写锁。HashMap桶中存储的是TreeNode
结点,这里的根本原因是==并发过程中,有可能因为红黑树的调整,树的形状会发生变化,这样的话,桶中的第一个元素就变了,而使用TreeBin
包装的话,就不会出现这种情况。 这种类型的节点, hash值为-2,从下面的构造函数中就可以看出来。这样我们通过hash值是否等于-2就可以判断桶中的节点是否是红黑树。nextTable
指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable
里进行查询节点,而不是以自身为头节点进行查找。在扩容操作中,我们需要对每个桶中的结点进行分离和转移,如果某个桶结点中所有节点都已经迁移完成了(已经被转移到新表 nextTable 中了),那么会在原 table 表的该位置挂上一个 ForwardingNode 结点,说明此桶已经完成迁移。这种类型的节点的hash值是-1,通过构造函数也可以看出来类中定义了三个静态的用于CAS操作的方法:
//获得在i位置上的Node节点
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);
}
//利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
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);
}
//利用volatile方法设置节点位置的值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
数组的初始化是在ConcurrentHashMap
插入元素的时候发生的,如调用put
等方法时发生的。初始化操作在initTable
方法中,该没有加锁,因为采取的策略是,当sizeCtl<0
时,说明已经有线程在给扩容了,这个线程就会调用Thread.yield()
让出一次CPU执行时间。看代码,我们可以看到上面说的sizeCtl
的作用:负数表示正在扩容,扩容完成后,用来表示阈值。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//tab为空时才进行初始化
while ((tab = table) == null || tab.length == 0) {
//如果sizeCtl<0,说明有其他的线程正在初始化,当前线程让出资源
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);//相当于0.75*n 用sizeCtl来表示阈值
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
整个put过程,主要在putVal
函数中实现,具体过程为:
ForwardingNode
,所以这种情况下,你尽管放,放了以后,扩容的线程总会遍历到这个节点,然后将这个节点迁移到新数组中。ConcurrentHashMap
,是分段锁,锁住很多的桶,所以并发效率更高。整个putVal
函数的代码如下,我对重要的地方都做了注释,应该很容易看懂:
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 (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
ConcurrentHashMap.Node<K,V> f; int n, i, fh;
//哈希表未初始化,则先对数组进行初始化操作,见上面的初始化操作
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果桶中元素为空,则直接通过CAS操作放到对应位置
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果有线程正在扩容,则两个线程一起帮忙扩容,扩容完毕后tab指向新table
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 (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&//找到对象,将其val替换
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
ConcurrentHashMap.Node<K,V> pred = e;
if ((e = e.next) == null) {//没找到,插入到尾部
pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
value, null);
break;
}
}
}
//红黑树节点的插入
else if (f instanceof ConcurrentHashMap.TreeBin) {
ConcurrentHashMap.Node<K,V> p;
binCount = 2;
if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//binCount不为0说明插入了新节点,为0说明在空桶中插入了一个节点(这种情况不需要树化)
if (binCount != 0) {
//默认桶中结点数超过8个数据结构会转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//更新size,扩容检测
addCount(1L, binCount);
return null;
}
和HashMap一样,ConcurrentHashMap内部也有一个数字,用来记录当前容器中有多少个键值对,但HashMap比较简单,就用一个size
记录,然后插入删除元素的时候变化就行了。但ConcurrentHashMap就很复杂了,这也是第一个让人大呼厉害的地方。
ConcurrentHashMap使用一个long型名为baseCount
变量和一个CounterCell
数组类型的名为counterCells
的变量一起来记录size。首先来看一下CounterCell
类的定义:
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
很简单,里面就一个long
类型的名为value
的变量。那他是怎么和baseCount
配合来记录元素数量的呢?先说结论,counterCells
可以看成是一个小型的HashMap,每个桶中的位置存储的CounterCell类型的变量,记录了在桶中的线程需要增加的size的值。有点绕,大概解释一下,比如有两个线程,他们都向ConcurrentHashMap中添加元素了,然后他们都需要去更新ConcurrentHashMap的baseCount
属性,那这时候,两个个线程会通过CAS
操作竞争去给baseCount
加1,这样竞争的话会一直自旋,很浪费性能对吧,那我这样,建立一个CounterCell
类型的数组counterCells
,然后每个线程都能生成一个随机数,然后我用这个随机数当这个线程的哈希码,然后通过这个哈希码,就能把这个线程对应到counterCells
数组中的一个位置对吧,我现在这个位置上,先把要更新的值更新到这个位置上的CounterCell
的value
值上面,最后,我再同步到baseCount上就好了。真的很绕,可以多读几遍理解一下。我们来看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;
}
从上面是可以看到的,最后size的计算方式,是用baseCount
的值,加上counterCells
中每个元素的value
值得到的,印证了我们上面的解释。
再来看putVal
中的最后一行,调用了addCount
这个函数,这个函数的作用就是更新容器的size
值,addCount
里面又调用了fullCount
这个函数。具体的代码如下:
private final void addCount(long x, int check) {
//这里的作用是,对hashMap的size进行更新,更新的时候,为了防止多个线程竞争更改baseCount的值,会将多个线程分散到CounterCell数组里面,对cell中的value值进行更改,最后再同步给baseCount
CounterCell[] as; long b, s;//@解释1
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 ||
//通过ThreadLocalRandom.getProbe() & m算出在CounterCell中的下标
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
//尝试给CounterCell.val加1
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
//更新baseCount的值
s = sumCount();
}
//check就是结点数量,有新元素加入成功才检查是否要扩容。
if (check >= 0) {//@解释③
Node<K,V>[] tab, nt; int n, sc;
//s表示加入新元素后容量大小,计算已省略。
//新容量大于当前扩容阈值并且小于最大扩容值才扩容,如果tab=null说明正在初始化,死循环等待初始化完成。
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
//sc<0表示已经有线程在进行扩容工作
if (sc < 0) {
//条件1:检查是对容量n的扩容,保证sizeCtl与n是一块修改好的
//条件2与条件3:应该是进行sc的最小值或最大值判断。
//条件4与条件5: 确保tranfer()中的nextTable相关初始化逻辑已走完。
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)) //有新线程参与扩容则sizeCtl加1
transfer(tab, nt);
}
//没有线程在进行扩容,将sizeCtl的值改为(rs << RESIZE_STAMP_SHIFT) + 2),原因见下面sizeCtl值的计算分析。
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
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表示需要扩容,false表示不扩容
//wasUncontended为true表示冲突,false表示不冲突
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
// 通过该值与当前线程probe求与,获得cells的下标元素,和hash 表获取索引是一样的
if ((a = as[(n - 1) & h]) == null) {
//cellsBusy=0表示counterCells不在初始化或者扩容状态下
if (cellsBusy == 0) { // Try to attach new Cell
//构造一个CounterCell的值,传入元素个数
CounterCell r = new CounterCell(x); // Optimistic create
//通过cas设置cellsBusy标识,防止其他线程来对counterCells并发处理
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
//将初始化的r对象的元素个数放在对应下标的位置
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;
}
//说明在addCount方法中cas失败了,并且获取probe的值不为空
else if (!wasUncontended) // CAS already known to fail
//设置为未冲突标识,进入下一次自旋
wasUncontended = true; //这里设置为true后,会跳到ThreadLocalRandom.advanceProbe(h);重新哈希
//由于指定下标位置的cell值不为空,则直接通过cas进行原子累加,如果成功,则直接退出
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
//如果已经有其他线程建立了新的counterCells或者CounterCells大于CPU核心数(很巧妙,线程的并发数不会超过cpu核心数)
else if (counterCells != as || n >= NCPU)
//设置当前线程的循环失败不进行扩容
collide = false; // At max size or stale
//恢复collide状态,标识下次循环会进行扩容
else if (!collide)
collide = true;
//给线程生成一个新的哈希值以后,还冲突,说明竞争激烈,将counterCells的容量扩展成原来的一倍
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
}
//wasUncontended=true,此时生成一个全新的线程hash,重新自旋
h = ThreadLocalRandom.advanceProbe(h);
}
//counterCells为空,就开辟数组,初始数组大小是2
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;
}
//各种条件都不满足的时候,尝试着直接去更新baseCount的值
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
解释1: 先看addCount
中的第一部分。这部分首先通过CAS操作去更新baseCount
,如果失败,那说明有多个线程在竞争更新baseCount
,那就尝试去更新counterCells
中的value
,这里要解释一下,as[ThreadLocalRandom.getProbe() & m])
这里,ThreadLocalRandom.getProbe()
就是给线程生成一个随机数作为线程的唯一标志,并且这个方法对于同一个线程每次生成的值是一样的。ThreadLocalRandom.getProbe() & m
这个代码是不是很熟悉?对!这不就是上一节HashMap中,知道key的hash值,然后计算其在数组中的位置的代码么。那我说counterCells
是一个小型的HashMap没问题吧。如果尝试更新counterCells
中的value
失败,就会进入到fullAddCount
方法。
解释2: fullAddCount
方法,应该是这个addCount
过程的精华了,你细细读,会发现作者为了提高性能,真的是将每一种情况都考虑到了,而且,里面if-else
的顺序非常非常讲究。我这里直接说一下执行的顺序,具体的自己再详细看上面的源码对照过程理解。
counterCells
这个数组应该是空的,那就会跳到下面这段代码,可以看到,这里是先生成了一个大小为2的数组,赋值给了counterCells
,然后通过h & 1
操作,创建一个初始值为x(要更新的量)的CounterCell
放在counterCells
对应位置上。//counterCells为空,并且没有别的线程占用counterCells(cellsBusy=0),就开辟数组,初始数组大小是2
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;
}
(n - 1) & h
计算得到的线程对应的桶为空,那直接new
一个初始值为x
的CounterCell
,放到桶中。对应的代码为: 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
}
}
wasUncontended=false
,那就通过下面的代码,将wasUncontended
更新为true
,说明已经冲突了,开启下一次循环else if (!wasUncontended) // CAS already known to fail
wasUncontended = true;
counterCells
中的线程所在位置的value
值,如果成功了,那就更新成功,break
;如果失败,进入下一次循环。对应代码为:else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
counterCells
一直冲突,需要扩容,会将collide
设置为true
,然后进行下一次循环。对应代码为:else if (!collide)
collide = true;
counterCells
的容量扩大到原来的2倍,和HashMap很像有没有!扩容的代码如下: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
}
好了,整个过程大概说完了,但是还有一些需要注意的东西。
注意点1: h = ThreadLocalRandom.advanceProbe(h);
这句代码的作用是,给线程生成一个新的随机数作为ID(区别ThreadLocalRandom.getProbe()
,它生成的值一直是不变的,但是一旦ThreadLocalRandom.advanceProbe(h)
生成新的值了,那ThreadLocalRandom.getProbe()
得到的也就是新的值了)。从这里我们看出,冲突了以后,并不是马上去扩容,而是先将线程的hash值变一变,让他映射到counterCells
的其他位置,看再有没有冲突。 而且,做后面的wasUncontended
还有collide
这几个变量之前,每次循环,前面的像CAS更新counterCells
中的线程所在位置的value
值这些动作都是还要做的,这就保证了在最坏的情况下,才会去扩容counterCells,这样就能节约内存资源。现在看,是不是要大呼厉害。
注意点2: counterCells的扩容是一直扩下去吗?它的容量是无限增大的吗?不是的,我们还有一句代码没有分析。这句话的意思是:如果其他线程也在扩容(对应counterCells != as
条件)或者counterCells
的数量已经达到CPU的核心数时(对应n >= NCPU
条件)时,就将collide
置为false
,这样就再不能扩容了。继续大呼厉害!
else if (counterCells != as || n >= NCPU)
collide = false;
解释3: 扩容条件的判断。这里的部分,看代码中的注释,应该都能看懂,这里借助resizeStamp
函数说一下sizeCtl
的一些知识。首先看resizeStamp
的实现。
/**
* The number of bits used for generation stamp in sizeCtl.
* Must be at least 6 for 32bit arrays.
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
Integer.numberOfLeadingZeros(n)
用于计算n转换成二进制后前面有几个0。(1 << (RESIZE_STAMP_BITS - 1)
即是1<<15,表示为二进制即是高16位为0,低16位为1:
0000 0000 0000 0000 1000 0000 0000 0000
那比如输入的n为8,其前导0的个数就是28,那得到的结果就是
1000 0000 0001 1100
这是一个很大的负数。 resizeStamp(n) 其实返回的是对 n 的一个数据校验标识,占 16 位。而 RESIZE_STAMP_SHIFT 的值为 16,那么位运算后,整个表达式必然在右边空出 16 个零。也正如我们所说的,sizeCtl 的高 16 位为数据校验标识,低 16 为表示正在进行扩容的线程数量。
(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2
表示当前只有一个线程正在工作,相对应的,如果 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
,说明当前线程就是最后一个还在扩容的线程,那么会将 finishing 标识为 true,并在下一次循环中退出扩容方法。
上面说了put
操作的过程,这里再来看扩容的过程。扩容的过程又是一个让人大呼牛逼的部分。因为,它实现了并发迁移的过程,就是当一个线程对ConcurrentHashMap
进行put
或者remove
等操作时,如果发现在扩容,那我这个线程就先不去做我的工作了,我就去帮助正在扩容的线程去迁移数据。在上面的put
里面,出现了helpTransfer
这个函数,就是去帮助扩容,put
操作的最外层是一个死循环,所以帮助扩容完成后,它会继续完成自己的put
操作。
每个线程承担不小于 16 个桶中的元素的扩容,然后从右向左划分 16 个桶(不一定能够16个)给当前线程去迁移,每当开始迁移一个桶中的元素的时候,线程会锁住当前槽中列表的头元素,扩容完成后会将这个桶中的节点设置为ForwardingNode
。假设这时候正好有 get
请求过来会仍旧在旧的列表中访问,如果是插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode
,就表示正在扩容,那当前线程会加入扩容大军帮忙一起扩容,扩容结束后再做元素的更新操作。下面来看代码:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//根据CPU核数计算每个线程的负责扩容的步长stride,最小步长为MIN_TRANSFER_STRIDE=16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
if (nextTab == null) {
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这个变量在旧的桶的末尾
transferIndex = n;
}
int nextn = nextTab.length;
//扩容时的特殊节点,标明正在扩容,且当前位置已经迁移完成,扩容期间的元素查找要调用其find()方法在nextTable中查找元素。
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;//当前线程是否需要继续寻找下一个可处理的节点
boolean finishing = false; //所有桶是否都已迁移完成。
//i指当前迁移任务的开始下标,bound为当前迁移任务的结束下标
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//这个while循环体的作用就是在控制i-- 通过i--可以依次遍历原hash表中的节点
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
//迁移总进度<=0,表示所有桶都已迁移完成。
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//通过CAS控制只有一个线程拿到bound~i间元素的迁移权利
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//计算出bound~i的范围,这个范围是该条线程负责迁移的范围
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {//如果所有的节点都已经完成复制工作 就把nextTable赋值给table 清空临时对象nextTable
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);//扩容阈值设置为原来容量的1.5倍 依然相当于现在容量的0.75倍
return;
}
//当前线程已结束扩容,sizeCtl-1表示参与扩容线程数-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
}
}
//如果遍历到的节点为空 则放入ForwardingNode指针
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//如果遍历到ForwardingNode节点 说明这个点已经被处理过了 直接跳过这里是控制并发扩容的核心
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//节点上锁
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
//如果fh>=0 证明这是一个Node节点
if (fh >= 0) {
//使用fn&n可以快速把链表中的元素区分成两类,A类是hash值的第X位为0,B类是hash值的第X位为1,
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);
}
//在nextTable的i位置上插入一个链表
setTabAt(nextTab, i, ln);
//在nextTable的i+n的位置上插入另一个链表
setTabAt(nextTab, i + n, hn);
//在table的i位置上插入forwardNode节点 表示已经处理过该节点
setTabAt(tab, i, fwd);
//设置advance为true 返回到上面的while循环中 就可以执行i--操作
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;
}
}
//如果扩容后已经不再需要tree的结构 反向转换为链表结构
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;
}
}
}
}
}
}
在并发扩容的过程中,最重要的是确定每个线程负责迁移的边界,每个线程负责不同的范围,这样就不会发生冲突。那我们先来以单个线程来看边界的确定过程。我们假设每个线程负责迁移2个桶(实际每个线程负责的桶最小是16个,我们这里为了方便看2个的情况),旧的数组长度为8,扩容后的长度为16。
i=bound=0
,transferIndex=8
,然后通过nextBound = (nextIndex > stride ? nextIndex - stride : 0)
这里将nextBound
初始化为nextIndex - stride
,我们的举例中就是7-2=5
,然后通过bound = nextBound; i = nextIndex - 1;
得到该线程扩容的范围为5~7
,如下图所示:advance
设置为false
,现在就是要从后往前遍历,将bound~i
之间的元素都迁移到新的数组中。然后就开始遍历。遍历的过程中,有以下几种情况:
i < 0 || i >= n || i + n >= nextn
:说明扩容完成了,将nextTable
置空,然后将table
赋值为nextTable
(f = tabAt(tab, i)) == null
:说明当前位置为空,不需要迁移数据,但是需要将此位置设置为ForwardingNode
类型。(fh = f.hash) == MOVED
:说明当前的节点已经是ForwardingNode
类型的节点了,说明当前的bound~i
内的元素已经有线程在处理了,那就设置advance = true
,重新去更新bound~i
。setTabAt(tab, i, fwd);
这一句,将这个桶进行标记。然后将advance
设置为true
。while
循环,然后主要是执行下面这一句,将i--
继续循环遍历。if (--i >= bound || finishing)
advance = false;
多线程的话,其实和单个线程很像,多线程的边界,通过volatile
修饰transferIndex
确定,很容易理解,。放一个扩容的大致流程图先体会一个(图来自别人的博客,我找不到是哪篇博客的了,这里感谢原作者):
这里来看一下,节点的拆分问题。在上一篇博客的HashMap中,我们看到了将一个链表或红黑树通过高位分成了两个部分,这里也一样,也是分成两个部分,但是,这个分割的过程,做了优化。看下面这部分的代码:
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);
}
比如有这样一个ConcurrentHashMap
它其实是通过lastRun
记录最后需要处理的节点,即为6号节点(此处,6号节点以后的节点,都是红色节点,这样在创建新链表的过程中,将6号前面的部分反转,后面的部分直接接到前面反转完成的部分就行了),A类和B类节点可以分散到新数组的槽位14和30中,在原数组的槽位14中,蓝色节点第X为0,红色节点第X为1,把链表拉平显示如下:
扩容后经过拆分,变成了两个部分:
再来一个链表拆分的图(图的来源见水印):
来一个红黑树拆分的图(图的来源见水印):
在get的过程中,桶中的元素可能有以下三种类型:
TreeBin
中的find
方法去查找。其中,TreeBin
里面是实现了读写分离锁的。TreeBin通过root属性维护红黑树的根结点,因为红黑树在旋转的时候,根结点可能会被它原来的子节点替换掉,在这个时间点,如果有其他线程要写这棵红黑树就会发生线程不安全问题,所以在ConcurrentHashMap中TreeBin通过waiter属性维护当前使用这棵红黑树的线程,来防止其他线程的进入。下面是一些锁的标识:ForwardNode
类型,说明此时链表正在迁移,但是此节点已经迁移完成了,所以,就通过ForwardNode
中的nextTable
属性,获取新表,在新表中去查找。查找的代码如下: 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时不需要加锁,而红黑树需要加锁: 在更新期间链表遍历总是可以进行的,但是树遍历不行,因为树旋转时可能会改变根结点或者其链接。而且,链表中成员都是value
和next
都是volatile
修饰的,所以当对链表中的元素有修改时,对其他线程是可见的。
最后,贴一下get
方法的源码:
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) //在迁移或都是TreeBin
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;
}
remove
方法和put
方法很像,他其实是调用了一个replaceNode
的方法,然后将对应的value
设置为null
。代码如下
public boolean remove(Object key, Object value) {
if (key == null)
throw new NullPointerException();
return value != null && replaceNode(key, null, value) != null;
}
replacaNode
方法的实现如下,可以看到,最后,需要通过addCount
方法去更新size
,只不过这里的参数是负数。具体我再不细说了。
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
ConcurrentHashMap.Node<K,V> f; int n, i, fh;
//map中不包含key的元素
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 (ConcurrentHashMap.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 ConcurrentHashMap.TreeBin) {
validated = true;
ConcurrentHashMap.TreeBin<K,V> t = (ConcurrentHashMap.TreeBin<K,V>)f;
ConcurrentHashMap.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)
//最后,去更新size
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
最后,对ConcurrentHashMap做一个总结: