ConcurrentHashMap是一种支持并发操作的HashMap,并发控制方面,它使用更小粒度的锁——对每个哈希桶的头节点加锁。虽然这样使得效率更高,能让读写操作最大程序的并发执行,但也造成了读写操作的一致性很弱,比如size()
返回的大小可能已经与真实大小不一样,比如clear()
调用返回后Map中却拥有着元素。
JDK8的ConcurrentHashMap并不是完美的,在阅读源码之前,建议先看看ConcurrentHashMap有些什么bug,以免在这些地方钻牛角尖。
JUC框架 系列文章目录
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
ConcurrentHashMap中的正常节点的hash值都会是>=0
的数,但还有三种特殊节点,它们的hash值可能是上面的三个值。
MOVED
,代表此节点是扩容期间的转发节点,这个节点持有新table的引用。TREEBIN
,代表此节点是红黑树的节点。RESERVED
,代表此节点是一个占位节点,不包含任何实际数据。//存放Node的数组,正常情况下节点都在这个数组里
transient volatile Node<K,V>[] table;
//一个过渡用的table表,在扩容时节点会暂时跑到这个数组上来
private transient volatile Node<K,V>[] nextTable;
//计数器值 = baseCount + 每个CounterCell[i].value。所以baseCount只是计数器的一部分
private transient volatile long baseCount;
//1. 数组没新建时,暂存容量
//2. 数组正在新建时,为-1
//3. 正常情况时,存放阈值
//4. 扩容时,高16bit存放旧容量唯一对应的一个标签值,低16bit存放进行扩容的线程数量
private transient volatile int sizeCtl;
//扩容时使用,平时为0,扩容刚开始时为容量,代表下一次领取的扩容任务的索引上界
private transient volatile int transferIndex;
//CounterCell相配套一个独占锁
private transient volatile int cellsBusy;
//counterCells也是计数器的一部分
private transient volatile CounterCell[] counterCells;
table
。存放节点的数组,只有第一个插入节点时才初始化,默认容量为16。扩容会让容量变成2倍,即保持为2的幂。nextTable
。扩容时需要使用,暂存新table,扩容完毕时,会把新table赋值给table
。sizeCtl
。如果使用ConcurrentHashMap的无参构造器,那么它默认为0。其他的值,它都有不同的含义:
transferIndex
。扩容时每个线程通过CAS修改该成员,来领取扩容任务。baseCount
和counterCells
。是计数器的实现依赖,类似于LongAdder
,它利用ThreadLocalRandom的探针机制来避免频繁的CAS失败,从而减少了因CAS失败而产生的自旋。Node
。其中val
域是volatile的,因为可以执行替换操作。next
也是volatile的,因为可以执行删除操作,导致链表结构发生变化。TreeBin
,但它不是真正的红黑树的root节点。它的root
成员才是红黑树根节点。也就是说,TreeBin
封装了TreeNode
,这是有好处的,因为红黑树随着平衡操作,根节点随时可能发生变化,但用TreeBin
进行封装,可以让数组成员不会发生变化。
TreeNode
。只有当数组容量>=64
且单个链表长度>=8
,才会让这个链表转换为红黑树。ForwardingNode
是扩容期间的转发节点,这个节点持有新table的引用。ReservationNode
是占位节点,不包含任何实际数据。在computeIfAbsent
和compute
方法中使用。 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;
}
首先要知道,这里的initialCapacity + (initialCapacity >>> 1) + 1
其实是一个bug,正确的做法应该是下面这个构造器的做法。
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);
//获得cap的正确做法
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
put
方法既是难点也是重点。难点在于它涉及到很多操作:初始化数组、插入动作、计数增加、扩容。
public V put(K key, V value) {
return putVal(key, value, false);
}
putVal
是put
和putIfAbsent
两个public方法的真正实现。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//根据原始hash值,获得处理后的hash值
int binCount = 0;//将代表一个哈希桶内节点数
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果发现table还没初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果发现索引所在元素为null,那么就CAS该null元素
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
}
//执行到这里,索引所在元素不为null
//如果发现元素的hash值为MOVED
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);//帮忙扩容转移
//如果发现元素的hash值为其他情况
else {
V oldVal = null;
synchronized (f) {//必须先锁住该数组元素
if (tabAt(tab, i) == f) {//锁住后,需要检查它是否还在这个索引上
//说明f是一个普通节点
if (fh >= 0) {
binCount = 1;//f是链表的头节点,把f自己先考虑进去
for (Node<K,V> e = f;; ++binCount) {
K ek;
//发现有重复key,根据onlyIfAbsent决定是否替换,
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;//不管接下来替不替换,oldVal都会被赋值
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;//binCount不会算上新增的节点
}
}
}
//如果f是一个TreeBin
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;//保持为2
//很明显,putTreeVal有两种可能:
//1. 新建节点,返回值为null
//2. 找到重复节点,但不执行替换操作,只是返回重复的节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;//不管接下来替不替换,oldVal都会被赋值
if (!onlyIfAbsent)
p.val = value;//替换操作
}
}
}
}
//不等于0,说明执行了新增操作,或替换操作(可能替换没有执行,因为onlyIfAbsent)
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);//将链表转换成红黑树
if (oldVal != null)//说明找到了重复节点,至于替换与否,根据onlyIfAbsent来
return oldVal;
break;
}
}
}
addCount(1L, binCount);//size增加1,并可能扩容table
return null;//说明肯定是新建操作
}
putVal
是写操作,当定位到某个非空的哈希桶,需要对这个哈希桶的头节点synchronized
加锁。相反,当定位到的是空的哈希桶,则只需要CAS修改就好了。
TreeBin
节点封装红黑树的root节点(将root节点作为它的成员),这样,即使红黑树因为平衡操作而改变root,也只是改变了TreeBin
节点的一个成员而已。所以对TreeBin
节点加锁好使。之所以红黑树的binCount固定为2,是因为:
binCount >= TREEIFY_THRESHOLD
)。addCount(1L, binCount)
的第二个参数为2,保证之后能进行扩容检查。putVal
执行的新建节点的操作。putVal
检测到了重复节点,至于替换与否,根据onlyIfAbsent决定。
onlyIfAbsent
为false,将替换。否则,不替换。 static final int HASH_BITS = 0x7fffffff; // 后面有31个1,相当于符号位固定为0,
static final int spread(int h) {
//低16位将受到高16位的扰动,& HASH_BITS后肯定为正数
return (h ^ (h >>> 16)) & HASH_BITS;
}
该函数用来初始化null的table,它可能会被两个线程并发执行。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//如果有其他线程正在初始化(sizeCtl为-1)
//或扩容(sizeCtl为其他负值)
//那么让出cpu,下一次循环可能还是进入这个分支,所以将会是自旋的过程
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//如果大于等于0,说明sizeCtl暂存着容量呢
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//先尝试修改sizeCtl
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//如果sc不为0,那么肯定是一个合理的容量
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);//这里是阈值,n * 0.75
}
} finally {
sizeCtl = sc;//将sizeCtl设置为阈值
}
break;
}
}
return tab;
}
sizeCtl
为-1,代表正在初始化table。sizeCtl
设置为-1,相当于一把锁,专门用来初始化table的锁。sizeCtl
将设置为阈值,即容量*0.75。相当于释放锁。在发现table数组元素是一个ForwardingNode时(MOVED),调用此函数帮忙扩容,但此函数只是尝试获得许可(U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)
),真正的扩容操作还是由transfer
完成的。
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) &&//检查f是否还是ForwardingNode
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {//从f的成员获得nextTab
//一旦进入此分支,返回的肯定是新table
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;
//可能一直循环,直到CAS成功为止
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {//尝试增加1的线程数
transfer(tab, nextTab);//执行完transfer后则退出
break;
}
}
return nextTab;
}
//上面if分支没有进入,说明扩容在进入if分支前就已经完成了,table成员已经是扩容后的新table
return table;
}
参数n是table的容量,容量肯定是2的幂。该函数根据一个容量n,得到与n唯一对应的一个标签值,这个标签值只需要16bit存储。
private static int RESIZE_STAMP_BITS = 16;
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
我们知道,table的最小容量是16即 2 4 2^4 24,最大容量是 2 30 2^{30} 230。而numberOfLeadingZeros
用来计算出第一个1前面的0的个数。
0000 0000 0000 0000 0000 0000 0001 0000
,所以numberOfLeadingZeros
返回27。0100 0000 0000 0000 0000 0000 0000 0000
,所以numberOfLeadingZeros
返回1。Integer.numberOfLeadingZeros(n)
的取值范围其实就是1~27。Integer.numberOfLeadingZeros(n)
都不一样。这就是唯一对应。1 << (RESIZE_STAMP_BITS - 1)
就是把1左移15位,所以就是1000 0000 0000 0000
。而最大的27都不能占据到第16个bit,所以与1000 0000 0000 0000
与一下,不会影响原数的有效bit,只是会把第16个bit置1。
扩容时,resizeStamp
的返回值将作为sizeCtl
的高16bit使用,这样sizeCtl
的符号位肯定为1,此时sizeCtl
肯定为负数。
并且由于最大的27都不能占据到第16个bit,那么resizeStamp
返回值的bit不可能都为1,那么扩容时,sizeCtl
肯定不能为-1,因为-1是每个bit都为1,所以说明sizeCtl
为-1是能唯一代表当前在初始化中的。
sizeCtl的低16bit用作线程扩容的许可,有点像是Semaphore的作用。我们把低16bit当成一个无符号整数来看待即可,当对sizeCtl加1时,sizeCtl的低16bit就会加1;当对sizeCtl减1时,sizeCtl的低16bit就会减1。我们称低16bit的这个无符号整数,为sizeCtl的线程数,或者许可数。
每一个线程来扩容时都需要获得许可:
transfer
前需要获得2个许可。实际动作为,sizeCtl + 2。transfer
前需要获得1个许可。实际动作为,sizeCtl + 1。但每个线程执行完transfer
任务,归还许可时,动作都是sizeCtl - 1。这样,当最后一个线程归还许可后,sizeCtl的线程数必定为1。最后一个线程检测到sizeCtl为1时,会从尾到头再扫查一遍每个哈希桶。
之所以第一个开始扩容的线程需要获得2个许可,是因为作者希望在这 2 16 − 1 2^{16}-1 216−1的可能状态中,将一种状态用作扩容结束前的中间状态,这种中间状态,作者将从尾到头再扫查一遍每个哈希桶是否都完成了转移。(具体看transfer函数讲解)
首先要知道,这里有个bug。sc == rs + 1
这里,应该写成sc == (rs << RESIZE_STAMP_SHIFT) + 1
。sc == rs + MAX_RESIZERS
同理。
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
上面这个分支进入,说明不需要当前线程帮忙扩容了。
(sc >>> RESIZE_STAMP_SHIFT) != rs
,刚读取的sc
的容量标签值和参数tab
的容量标签值,说明在调用期间,table
成员已发生变化,那自然是因为扩容。sc == rs + 1
,扩容结束前的中间状态。sc == rs + MAX_RESIZERS
,许可已经被领完。transferIndex <= 0
,transfer任务已经被领完。(具体看transfer函数讲解)该函数首先判断操作应该是扩容,还是树化,但真正的树化操作交给了TreeBin
的构造器。
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)//如果数组容量还小,则优先扩容
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {//树化也是写操作,需要加锁
if (tabAt(tab, index) == b) {//再次检查是否为头节点
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
//尾插法,构件双向链表
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));//利用TreeBin构造器来树化
}
}
}
}
}
该函数判断需要扩容后,将开始扩容。扩容逻辑类似addCount
的扩容逻辑。
private final void tryPresize(int size) {//参数是旧数组的容量的二倍
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);//算出一个目标容量
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {//如果table为null,那么此时sizeCtl暂存着一个容量值
n = (sc > c) ? sc : c;//获得一个较大值
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
//执行到这里,table成员不为null
//如果目标容量比现有容量小,或现有容量已经到达最大容量。就没有必要扩容了
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
//如果table成员还没变,此时判断需要扩容
else if (tab == table) {
int rs = resizeStamp(n);//获得容量标签值
//如果sizeCtl已经小于0,说明扩容已经开始
if (sc < 0) {
Node<K,V>[] nt;
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);
}
//如果sizeCtl大于0,说明存的是阈值,说明扩容还没开始。由当前线程开始
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
统计ConcurrentHashMap的size的任务,交给了baseCount
成员和counterCells
数组成员。
counterCells
为null时,baseCount
的值就是size。counterCells
不为null时,baseCount
加上每个counterCells
的数组元素的值,才是size。之所以引入counterCells
数组,是因为如果每个线程都在CAS修改baseCount
这一个int成员的话,必定会造成大量线程因CAS失败而自旋,从而浪费CPU。现在引入一个counterCells
数组,让每个数组元素也来承担计数的责任,则线程CAS修改失败的概率下降了不少。
所以,当counterCells
不为null时,一定要去优先修改counterCells
的某个数组成员的值,而且由于利用了ThreadLocalRandom的probe探针机制来获得数组的随机索引,所以很大概率上,不同线程获得的数组成员是不同的。既然不同线程获得的数组成员不同,那么不同线程尝试CAS修改某个数组成员,肯定不会失败了,从而减小了线程的因CAS失败而导致的自旋。
了解了以上知识,我们再来看addCount
和fullAddCount
的实现,就好懂多了。
//参数x是需要增加的数量。 check用来判断是否需要检测,如果检测到需要扩容,就扩容。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
/*计数部分*/
if ((as = counterCells) != null || //如果counterCells成员不为null,短路后面条件
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {//如果counterCells成员为null,但CAS失败
CounterCell a; long v; int m;
boolean uncontended = true;
//进入下面这个分支很容易:
//1. 如果as为null 2. 如果as的大小为0(这不可能,只是一种保护)
//3. 如果as的随机索引位置上的元素还没初始化 4. CAS修改一个cell的值失败了
//不进入这个分支很难:只有当 as不为null,且大小不为0,且随机索引位置元素不为null,
//且修改这个cell元素的value成功了,才不会进入分支。
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;
}
//执行到这里,说明 修改某个cell的value成功了,计数已经成功加上去了。
//既然修改某个cell的value成功了,而且check参数还这么小,就直接返回,不用去做check动作了
if (check <= 1)
return;
//如果需要check,那么算出当前的size
s = sumCount();
}
//执行到这里,说明上面代码,要么CAS修改baseCount成功了,要么CAS修改某个cell的值成功了
//而且s已经是当前map的映射数量了。
/*扩容部分*/
if (check >= 0) {//如果需要check
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);//按照当前容量n,得到与n唯一对应的一个标签值
//如果正在扩容
if (sc < 0) {
//1. 如果sizeCtl得到的容量标签值,与之前得到的不一样
//2. 如果扩容线程数为1,说明扩容结束(因为第一个线程,设置线程数量为2,最多增长到MAX_RESIZERS)
//3. 如果扩容线程数为MAX_RESIZERS,说明可以帮忙扩容的线程的数量到达上限
//4. 如果nextTable为null,说明扩容结束
//5. 如果transferIndex <= 0,说明transfer任务被领光了,所有的哈希桶都有线程在帮忙扩容
//以上情况,都说明当前线程不需要去扩容操作了
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//如果看起来可以帮忙扩容,那么尝试增加一个线程数量
//上面分支每个条件都为false才可能执行到这里,说明nt肯定不为null
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
//修改成功,相当于拿到许可,开始扩容
transfer(tab, nt);//nt不为null
}
//如果sc大于0(这里其实不可能等于0),说明当前table没有在扩容,
//当前线程作为第一个线程,所以要设置线程数为2
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();//再次计算size,开始下一次循环。有可能下一次循环,还会继续扩容(如果s >= (long)(sc = sizeCtl))
}
}
}
我们把addCount
的逻辑分为计数部分和扩容部分。
计数部分的if分支有可能根本没有进入,因为当前counterCells
成员为null,且修改baseCount
成功了。此时计数部分已经完成了任务,并将s
局部变量设置为了map的size。
现在考虑进入计数部分的if分支,此时有两种情况:
counterCells
成员不为null,短路后面条件。这种情况,接下来会去尝试CAS修改某个cell的值。
fullAddCount(x, uncontended)
然后直接return掉。fullAddCount(x, uncontended)
传入的uncontended
参数此时必为false。(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)
失败)counterCells
成员为null,但CAS修改baseCount
失败。这种情况一定去执行fullAddCount(x, uncontended)
然后直接return掉。因为as == null
成立,直接短路后面条件。
fullAddCount(x, uncontended)
传入的uncontended
参数此时必为true。 (boolean uncontended = true
)所以,当调用fullAddCount(x, uncontended)
时,一定是因为之前,尝试CAS修改baseCount
失败、或者CAS修改某个cell的值失败了。
fullAddCount(x, uncontended)
传入的uncontended
参数为false代表,CAS修改某个cell的值失败了。
当计数部分结束时,s
局部变量的值就已经是map的size了。分为两种情况:
counterCells
成员为null,所以只需要增加baseCount
就好。s = sumCount()
算出来。因为这种情况,计数分布在baseCount
和counterCells
数组上。s >= (long)(sc = sizeCtl)
一定成立,因为扩容时sizeCtl
为负数。transfer
去做真正的扩容。从以上分析可知,不管是CAS修改baseCount
失败、或者CAS修改某个cell的值失败了,只要是失败了,之后都会执行fullAddCount
然后直接return,从而不去执行扩容部分。
只有CAS成功了,才有可能去调用扩容部分的代码。
这样的做法,可能会导致,该扩容的时候不会扩容。因为毕竟只有CAS直接成功的线程,才可能扩容。比如这种场景,线程A CAS成功,此时大小为阈值-1;然后线程B CAS失败,此时大小刚好为阈值,但由于失败,不会去扩容;只有等到第3个线程到来,才可能去扩容了。
当addCount
函数里CAS修改baseCount
失败、或者CAS修改某个cell的值失败了,会调用到fullAddCount
。该函数保证能够将x加到baseCount
或某个cell上去。
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
//线程第一次调用fullAddCount,肯定会进入以下分支,因为只有localInit后,probe才不可能为0
//之后调用fullAddCount,就不可能进入这个分支。
if ((h = ThreadLocalRandom.getProbe()) == 0) {//探针为0,说明localInit从来没有调用过
ThreadLocalRandom.localInit(); // 先初始化再说
h = ThreadLocalRandom.getProbe(); // 这句get到的就肯定不为0了
wasUncontended = true;
}
boolean collide = false;
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
//如果counterCells数组不为null,那么当然优先在某个数组元素增加x
if ((as = counterCells) != null && (n = as.length) > 0) {
//探针取余后的下标的元素为null
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { //这个cellsBusy相当于一个独占锁的state,为0说明可以获得锁
CounterCell r = new CounterCell(x); // 乐观地创建,因为假定获得锁后,该索引位置还是为null
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//尝试获得锁
boolean created = false;
try { // 获得锁后,还是需要检查该索引位置是否还是为null
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {//该索引位置为null,接下来赋值
rs[j] = r;
created = true;//赋值成功
}
} finally {
cellsBusy = 0;//最终无论如何,都要释放锁
}
if (created)//如果赋值成功,那么该函数任务完成
break;
continue; //执行到这里,说明赋值没有成功,因为获得锁后,发现该索引位置却不为null了
}
}
collide = false;
}
//执行到这里,说明探针取余下标的元素不为null
//从addCount的逻辑来看,只可能addCount里CAS修改某个cell失败了,
//才会导致addCount调用此函数的wasUncontended为false
else if (!wasUncontended) // 既然之前的探针冲突了,那么就执行advanceProbe获得下一个探针
wasUncontended = true;
//CAS修改当前探针指向的数组元素,如果成功了break
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
//如果counterCells更新了,或者数组大小大于CPU个数,短路后面所有if分支,优先移动探针
else if (counterCells != as || n >= NCPU)
collide = false;
//短路后面if分支,并消耗掉collide
else if (!collide)
collide = true;
//如果没有被前面短路,将进入扩容分支
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//前提是获得锁成功
try {
if (counterCells == as) {//需要再次检查counterCells成员没有变化
CounterCell[] rs = new CounterCell[n << 1];//扩容二倍
for (int i = 0; i < n; ++i)
rs[i] = as[i];//重复利用旧的成员,新位置为null
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // 扩容完事,不移动探针
}
h = ThreadLocalRandom.advanceProbe(h);
}
//执行到这里,说明之前检测到counterCells数组为null
//cellsBusy代表是否可以进行初始化操作
else if (cellsBusy == 0 && counterCells == as &&//这里counterCells和as都为null
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//将初始化许可设置为1
boolean init = false;
try { //执行初始化操作
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];//建立大小为2的数组
rs[h & 1] = new CounterCell(x);//探针取余得到下标,只设置一个数组元素(lazy init)
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
//执行到这里,说明之前检测到counterCells成员为null,且没有抢到初始化操作的。
//那就退而求其次,去设置baseCount好了。
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
不需要关心参数wasUncontended
传进来为true的情况,因为这种情况不会造成什么影响。
我们已知,当fullAddCount
的参数wasUncontended
为false的情况,一定是因为addCount
函数里CAS修改某个cell的值失败了(即有竞争)。但这个线程需要分为两种情况:
fullAddCount
,则一定会进入if ((h = ThreadLocalRandom.getProbe()) == 0)
,然后将wasUncontended
设置为true。
addCount
函数里通过ThreadLocalRandom.getProbe()
获得的探针为0,0是作为探针没有初始化的默认值的,也就是说,之前addCount
函数里CAS修改某个cell的值失败,很可能是因为没有获得到正常非零的探针值才导致的。所以,获得非零探针值后,就将wasUncontended
设置为true,代表之前的线程竞争其实不算事。fullAddCount
,则不会进入if ((h = ThreadLocalRandom.getProbe()) == 0)
,wasUncontended
不会被提前修改掉。
addCount
函数里通过ThreadLocalRandom.getProbe()
获得的探针为非零值,这是一个正常的探针值。正常的探针值,因为线程竞争导致CAS失败了,说明探针需要移动了。 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;
wasUncontended
为false时,这个变量开始起作用。简单的说,它是用来短路下一个if分支的,包括后面的所有if分支。wasUncontended
为false时,代码将不会去U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)
尝试修改某个cell的值。然后移动探针。 else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//扩容if分支
...
continue;
}
h = ThreadLocalRandom.advanceProbe(h);
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)
失败了,则没法进入扩容if分支,只好移动探针。再来看一下collide
的赋值情况:
boolean collide = false
的默认值为false,说明默认情况下,优先移动探针。 if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
...
}
collide = false;
}
collide
赋值为false,优先移动探针。 else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
counterCells
数组成员已经更新(必然是因为扩容,所以有了更大的位置可以赋值),或数组大小已经大于等于CPU数量(没必要优先扩容,因为理论上每个线程都有一个独有的数组成员了),则collide
赋值为false,优先移动探针。 else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//扩容if分支
...
collide = false;
continue;
}
collide
赋值为false,优先移动探针。cellsBusy相当于独占锁的state,当它为1时,代表锁被某个线程持有。当线程持有锁时,可能扩容counterCells
数组,也可能对某个null的数组成员赋值。
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];//重复利用
不用担心把x加到旧的数组成员上去,从而导致加的数字白加了。因为从扩容逻辑来看,旧的数组成员都被重复利用了。
transfer
是真正的扩容实现,从上文已知它有这么几种调用过程:
putVal ⇒ helpTransfer ⇒ transfer
。这种过程,当前线程肯定不是第一个获得扩容许可的那个线程。putVal ⇒ addCount ⇒ transfer
。这种过程,当前线程可能是第一个获得扩容许可的那个线程。putVal ⇒ treeifyBin ⇒ tryPresize ⇒ transfer
。这种过程,当前线程可能是第一个获得扩容许可的那个线程。U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
,将线程数量从0设置为2,且传递给transfer
的第二个参数为null,代表nextTable还没初始化。transfer
的第二个参数不为null,传递的就是nextTable。transfer
可以被多个线程同时调用,一起完成扩容的工作。而多线程一起工作,就意味着任务需要划分,而且各个线程不要相互干扰。
我们先来看看单线程的HashMap是如何扩容的:
i
槽位上去,高桶放到新数组的i + n
上去。先简单解释下为什么这么分离:
PS:这个图是我以前分析HashMap源码的博客图,大家关注一下这个分离原理就行。
0b10000
即16
,那么可能的table下标范围为0b0000 - 0b1111
,即能影响到元素所在table下标的bit只有后4位bit0b????
。XYZQ
,由于当前容量16
的限制,它们会被放置到同一个哈希桶(table下标为0bXYZQ
)里。0b100000
即32
,所以现在能影响到元素所在table下标的bit只有后5位bit0b?????
,但相比之前,只有右起第5位bit可能发生变化。0b10000
即16。既然每个哈希桶的元素在扩容后,要么留在 原table下标,要么留在 原table下标+旧容量 的新下标。那么说明分别处理不同哈希桶的两个线程是不会互相影响的。这样,就说明可以按照哈希桶为单位来划分transfer任务了。
但是线程调度也是有消耗的,所以领取一次任务肯定不可能只处理一个哈希桶。所以,我们根据CPU数算出来一个stride,但这个stride至少为16。这样,每个线程来领取到的任务就是:连续stride个的哈希桶。
而领取任务依赖的成员则是transferIndex
,如果CAS修改transferIndex
从当前值current
成功改成current-stride
,那么当前线程领取到的任务就是:[current-stride, current-1]
。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;//stride为步长,代表线程一次性转移多少个哈希桶
//stride可能等于 n ÷ 8 ÷ NCPU,但最小也得是MIN_TRANSFER_STRIDE
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
if (nextTab == null) { // 为null说明,nextTab还没初始化
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;//transferIndex成员初始为容量,但作为索引来说,这是个不可能的索引
}
int nextn = nextTab.length;//所以nextn是n的二倍
//初始化ForwardingNode节点,它持有新table的引用,旧table的一个哈希桶完全转移后,
//将用fwd替换该哈希桶,作为占位符使用,表明该哈希桶已经转移
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // 检测到每个哈希桶都转移后,还需要从尾到头扫查一遍,是不是每个节点都是ForwardingNode了
//循环将不断领取stride个哈希桶作为任务,范围则为[bound,i],自然i-bound+1 = stride(只要不是最后一次领任务)
//从i遍历到bound,每次遍历转移一个哈希桶到新table
for (int i = 0, bound = 0;;) {
//f 当前遍历的哈希桶的头节点; fh f的哈希值
Node<K,V> f; int fh;
/*移动遍历指针部分*/
while (advance) {//为true代表当前处理的哈希桶已经处理完毕,需要移动i和bound了
int nextIndex, nextBound;//nextIndex是i下一个可能的值,nextBound是bound下一个可能的值
if (--i >= bound || finishing)//移动遍历指针i,只要还在[bound,i]范围内,短路后面
advance = false;
else if ((nextIndex = transferIndex) <= 0) {//上面发现超过bound范围了,但没有任务可以领取了
i = -1;
advance = false;
}
//尝试领取stride个哈希桶作为任务
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//如果领取任务成功,更新[bound,i]
bound = nextBound;
i = nextIndex - 1;//右边界总是作为一个不在范围内的索引,所以需要减1(最开始transferIndex为容量)
advance = false;
}
//advance = false后,除非设置回true,否则不会进入该循环了
}
/*处理当前遍历哈希桶部分*/
//进入该分支说明所有的哈希桶都被领取完,但不一定当前都处理完毕了
if (i < 0 || i >= n || i + n >= nextn) {//其实只有第一个条件能成立,而且肯定是因为i = -1
int sc;
if (finishing) {//sweep检查的收尾工作
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//既然任务都被领取完,而且当前线程自己的任务也都做完了,那当前线程就可以归还许可了
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//如果不是最后一个归还许可的线程,直接return,因为不需要它们做sweep检查
//如果是最后一个归还许可的线程,不return,然后开始sweep检查
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // 从尾到头全部检查一遍
}
}
//当前遍历哈希桶为null,那好办,设置为ForwardingNode即可
else if ((f = tabAt(tab, i)) == null)
//如果设置失败,下一次循环还是检查同一个哈希桶,但不一定进这个分支
advance = casTabAt(tab, i, null, fwd);
//当前遍历哈希桶已经是ForwardingNode,不用处理
else if ((fh = f.hash) == MOVED)
advance = true; // 设置为true,遍历指针将移动
//如果遍历哈希桶是一个正常节点
else {
synchronized (f) {//锁住哈希桶的头节点
if (tabAt(tab, i) == f) {//需要再次判断f是否还为头节点
Node<K,V> ln, hn;
//一个哈希桶内的节点只可能分离到两个地方,一个是原地不动 i,一个是i + 旧容量
//称它们为低桶和高桶。分离前桶里既有低桶元素也有高桶元素,而lastRun则是从尾到头
//找到和最后一个元素连续同阶级的第一个元素。
//链表分离处理
if (fh >= 0) {
int runBit = fh & n;//要么为n,要么为0.分别代表处于high,还是low
Node<K,V> lastRun = f;
//该循环用来找到和最后一个元素连续同阶级的第一个元素。
for (Node<K,V> p = f.next; p != null; p = p.next) {
//从第二个元素开始遍历,因为第一个元素肯定和自己同阶级
int b = p.hash & n;//算出当前遍历元素是low还是high
if (b != runBit) {//如果当前遍历元素和当前lastRun元素的阶级不同
//替换为当前遍历元素
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {//找到的是低桶元素,将lastRun设置给ln
ln = lastRun;
hn = null;
}
else {//找到的是高桶元素,将lastRun设置给hn
hn = lastRun;
ln = null;
}
//头插法新建两个新链表,但lastRun以后包括它自己,都不用新建元素了
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);
}
//因为是头插法,所以ln 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;//lo为低桶head,loTail为低桶tail
TreeNode<K,V> hi = null, hiTail = null;//hi为高桶head,hiTail为高桶tail
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;
//hc为0,说明原哈希桶只有低桶元素,就不用再重新组织树结构了
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);//最后才把原槽位,赋值为fwd
advance = true;
}
}
}
}
}
}
前面的逻辑主要处理了nextTab
参数为null的情况,主要的逻辑还是集中在for (int i = 0, bound = 0;;)
循环里,将其分为:
for (int i = 0, bound = 0;;)
循环,第一次执行 移动遍历指针部分,进入else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0)))
分支(假设CAS直接成功,如果不成功,那就不停CAS直到成功),当前线程领取到的任务是[bound, i]
,设置advance为false,代表i
暂时不需要前进。i
需要前进了。if (--i >= bound || finishing)
分支,--i
就移动了指针,好处理下一个哈希桶。设置advance为false,代表i
暂时不需要前进。i
需要前进了。i == bound
。if (--i >= bound || finishing)
分支(执行了--i
,此时i + 1 = bound
),且发现还有剩余任务可以领取(else if ((nextIndex = transferIndex) <= 0)
分支没有进入,说明transferIndex
大于0,还有任务可以领),就再次CAS修改transferIndex
来领取任务,如果领取成功,那么又将重复以上所有步骤。首先得讲一下流程。既然都是最后完成任务的线程了,说明任务已经被领取完了,所以此时transferIndex
为0。
i == bound
,正在执行 处理当前遍历哈希桶部分。while (advance)
循环。if (--i >= bound || finishing)
不成立,进入下一个分支。此时i + 1 = bound
,finishing
为false。else if ((nextIndex = transferIndex) <= 0)
分支进入,因为此时transferIndex
为0。还设置了i = -1
。if (i < 0 || i >= n || i + n >= nextn)
分支,然后进入if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)
分支。这里的CAS操作是用来归还许可的,因为当初调用transfer
前获得了这个许可。由于当前线程是最后一个来归还许可的,所以归还之前许可数肯定为2,因为第一个调用transfer
的线程设置的许可数为2。而非最后一个归还许可的线程,归还之前许可数肯定大于2。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
分支,然后直接返回,因为它们不需要进行sweep检查。if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
分支,设置**finishing
为true**,好进行sweep检查。遍历指针重新设置为n,因为是从尾到头全部扫查一遍。finishing
为true将导致while (advance)
循环只会进入if (--i >= bound || finishing)
分支,即只会减小i
。此时前面的条件--i >= bound
成不成立都无所谓,只是为了--i
移动指针而已。for (int i = 0, bound = 0;;)
大循环里,先 移动遍历指针,再 检查当前遍历哈希桶。i
变成-1,又进入if (i < 0 || i >= n || i + n >= nextn)
分支,但此时finishing
为true,进入if (finishing)
分支,完成sweep检查的收尾工作。注意,最后一个完成任务的线程,不一定是领取到最左stride任务的那个线程,这个得看线程的执行顺序。
上面分析都是假设每个线程都能至少领到一次任务,所以可以说:最后一个完成任务的线程,将进行sweep检查。其实精确的说,应该是最后一个来归还许可的线程,将进行sweep检查。因为还有一种特殊情况,这种线程运气不好,进入transfer
后从来没有领取到过任务,即进入while (advance)
循环后每次CAS尝试修改transferIndex来领取任务都失败了,直到transferIndex变成0,然后将由这个线程来做sweep检查。
哈希桶分离的原理已经讲过,我们来看具体实现吧。
if (fh >= 0)
分支的逻辑,就是链表分离处理。分离前桶里既有低桶元素也有高桶元素,现在称低桶与低桶同阶级,高桶与高桶同阶段,那么lastRun则是用来从尾到头找到和最后一个元素连续同阶级的第一个元素。ln hn
则分别代表了低桶的lastRun(如果最后一个元素是低桶元素)、高桶的lastRun(如果最后一个元素是高桶元素)。
现在假设执行完第一个for循环for (Node
后,当前哈希桶的状态如上图第一个状态:可见,最后一个元素是高桶元素,lastRun指向了连续的也是高桶的那个元素。因为最后一个元素是高桶,所以hn
也指向lastRun,但ln
就只能是null了。
之后新建1节点时,新建节点和原节点的key、value都是指向同一个对象的。之后的过程,就是按照头插法新建链表的过程:新建一个低桶节点相连后,就会更新ln
;新建一个高桶节点相连后,就会更新hn
。
从上图最后一个状态可见,hn
就是新table的高桶的头节点,ln
就是新table的低桶的头节点。而且新建的两个哈希桶的链表结构,完全不影响旧哈希桶。只是,分离后的两个新哈希桶里的元素,顺序有所改变,一部分或全部,都会变成倒序。
lastRun的好处相信大家也看图能理解到了,因为从lastRun以后的节点包括它自己,是不需要新建节点的,这就减少了不必要的操作。
而上面两种特殊情况,就更加体现了lastRun的作用。这两种情况,哈希桶全是同阶级的节点,那么在逻辑里,第二个for循环for (Node
都不会去执行了。
红黑树分离则没有使用lastRun这种机制,在for循环中,每个旧节点它都会生成相应的新节点。然后低桶高桶都会重新组织一次红黑树结构。
不能使用lastRun机制,必须生成每一个新节点。
new TreeBin(lo)
重新组织树结构时,会打乱原哈希桶的树形结构,而这样就会影响到并发的读操作,所以,红黑树分离处理必须生成每一个新节点。 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;
红黑树分离处理同样处理了两种特殊情况。hc != 0
和lc != 0
两种情况。
从上面两种数据结构的分离操作来看,旧哈希桶的结构从来没有发生过变化。而对于读操作来说,有一个重要的时间节点,那就是处理哈希桶完毕时执行的setTabAt(tab, i, fwd)
。
setTabAt(tab, i, fwd)
之前,就获得i索引数组成员的引用,那么读操作就在原哈希桶里进行读操作。setTabAt(tab, i, fwd)
之后,此时已经执行过了setTabAt(nextTab, i, ln)
和setTabAt(nextTab, i + n, hn)
,说明低桶和高桶已经放到了新table上去了。所以,读操作可以通过ForwardingNode得到新table,从而在新table里继续读操作。nextTable
从null更新为二倍大小数组,transferIndex
从0更新为n。transferIndex
不断减小,随着任务的领取。nextTable
成员不断更新,随着每个旧哈希桶分离出来的高桶低桶的赋值。sizeCtl
不断减小,随着线程归还许可。transferIndex
变成0。sizeCtl
的线程数变成1。nextTable = null
,nextTable
成员置null。此时table数组的每个非null成员都是ForwardingNode。table = nextTab
,table
成员置为新数组。sizeCtl = (n << 1) - (n >>> 1)
,此时sizeCtl才置为阈值。之前的步骤中,sizeCtl都还为负数。可见在某些步骤上,其他线程可能会发现ConcurrentHashMap处于一种中间状态上。
根据Doug Lea在[concurrency-interest]的回答可知,这个sweep扫查机制是之前版本遗留下来的,本来是可以删除掉的。
删除掉没有任何影响,因为当最后一个线程来归还许可时,这意味着其他线程已经归还了许可。且只有在当前线程 完成了领取的任务后,才会归还许可,所以当最后一个线程来归还许可时,每个哈希桶都已经完成了转移,所以说最后的sweep检查是没有必要的。
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)))//需要判断是否equal相等
return e.val;
}
//小于0说明是一个特殊节点
//1. 为ForwardingNode节点,说明当前在扩容中,需要转发到nextTable上寻找
//2. 为TreeBin节点,说明哈希桶是红黑树结构,需要二叉查找
//3. 为ReservationNode节点,说明当前槽位之前是null,这里只是占位,所以直接返回null
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;
}
}
//没有找到,则返回null
return null;
}
该函数在开头并没有检查参数key
是否为null,这是个隐患,不过int h = spread(key.hashCode())
这里会给调用者抛出空指针异常。
返回null代表没有找到这个key。
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;;) {//直接去nextTable上寻找
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||//如果
(e = tabAt(tab, (n - 1) & h)) == null)//如果哈希桶为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//只可能是红黑树节点,或者ReservationNode节点
return e.find(h, k);
}
//如果不是特殊节点,那就是普通链表的节点,后移e
if ((e = e.next) == null)
return null;
}
}
}
}
ForwardingNode
节点持有nextTable,所以我们直接去nextTable上寻找。ForwardingNode
节点时,continue外层循环,以避免递归层数太高。 public V remove(Object key) {
return replaceNode(key, null, null);
}
先提前说下参数:
key
,你想要的key。cv
,如果找到了key,对这个映射的value的要求。
cv
为null,代表没有要求,或者肯定满足要求。即找到了满足要求的映射。cv
不为null,必须判断cv
和旧value是equal的,才能算找到了满足要求的映射。value
,在找到了满足要求的映射后,如果value
为null,说明执行删除操作。如果value
不为null,说明执行替换操作。根据以上讲解,replaceNode(key, null, null)
代表,只有能找到包含key的映射,不用管旧value,然后删除掉这个映射。
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)))) {//判断key相等
V ev = e.val;
//cv为null代表对这个映射的value没有要求
//cv不为null,则需要比较两个value是否相等
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)//想替换过去的新value不为null,才能替换
e.val = value;
else if (pred != null)//没法替换,只好执行链表的删除操作
pred.next = e.next;
else//没法替换,且当前是头节点,只好新设置头节点
setTabAt(tab, i, e.next);
}
break;//找到key,最终都会退出循环
}
//没有找到key
pred = e;//保存前驱
if ((e = e.next) == null)//移动e
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;
//cv为null代表对这个映射的value没有要求
//cv不为null,则需要比较两个value是否相等
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)//想替换过去的新value不为null,才能替换
p.val = value;
else if (t.removeTreeNode(p))//没法替换,只好执行红黑树的删除操作
//返回true,代表应该反树化
//返回false,那就啥也不干就好
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {//如果保存了旧值(只有找到满足要求的映射,oldVal才会保存下来)
if (value == null)//想替换过去的新value为null,说明之前执行的删除操作
addCount(-1L, -1);//第一个参数代表计数减一,第二个-1代表不用检查扩容
return oldVal;
//只要找到了key,而且对应的value符合要求,那么不管是替换操作,还是删除操作,都返回旧值
}
break;
}
}
}
return null;//相反,没有找到key,那就肯定返回null
}
代码比较简单,代码思路和对参数的解释一致。
也有其他public函数调用到replaceNode
,只是实参有所不同。
//value为null,肯定返回false
public boolean remove(Object key, Object value) {
if (key == null)
throw new NullPointerException();
return value != null && replaceNode(key, null, value) != null;
}
//三个参数都不允许为null,否则空指针异常
public boolean replace(K key, V oldValue, V newValue) {
if (key == null || oldValue == null || newValue == null)
throw new NullPointerException();
return replaceNode(key, newValue, oldValue) != null;
}
//两个参数都不允许为null,否则空指针异常。对映射的value没有要求
public V replace(K key, V value) {
if (key == null || value == null)
throw new NullPointerException();
return replaceNode(key, value, null);
}
首先要了解到,当容量到达MAXIMUM_CAPACITY
后,就不会再扩容了。比如在addCount
里,需要满足(n = tab.length) < MAXIMUM_CAPACITY
的条件后,才可能去扩容。此时意味着,就算大小超过了阈值,table也不会变成二倍大小了。甚至于大小会超过,int的最大值。
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
//所以当发现大小已经超过int最大值,只能返回int最大值
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
所以当发现大小已经超过int最大值,只能返回int最大值。这是一个历史遗留问题,因为这个函数的返回值被定为int了。
public long mappingCount() {
long n = sumCount();//clear方法可能导致小于0
return (n < 0L) ? 0L : n; // ignore transient negative values
}
应该尽量使用mappingCount
得到一个long的大小。
public void clear() {
long delta = 0L; // negative number of deletions
int i = 0;
Node<K,V>[] tab = table;
while (tab != null && i < tab.length) {
int fh;
Node<K,V> f = tabAt(tab, i);
if (f == null)
++i;
else if ((fh = f.hash) == MOVED) {
tab = helpTransfer(tab, f);//帮忙扩容,索引从0重新开始
i = 0; // restart
}
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> p = (fh >= 0 ? f ://如果是链表节点
(f instanceof TreeBin) ?//如果是红黑树节点
((TreeBin<K,V>)f).first : null);//二者都不是,那就忽略
while (p != null) {
--delta;//累计
p = p.next;//不管是链表还是红黑树,最后都可以当作链表
}
setTabAt(tab, i++, null);//遍历完哈希桶,置空slot
}
}
}
}
if (delta != 0L)
addCount(delta, -1);//计数减去delta,第二个参数代表不用检查扩容必要性
}
由于加锁粒度很细(对每个哈希桶加锁),可能存在并发问题,delta
代表的计数可能因为并发的插入或删除,而变得“不正确”。这里是指,clear
遍历过程中,别的线程并发的插入或删除了clear
遍历过的哈希桶,从而导致不正确。
所以此函数可能返回负值,说明别的线程并发的删除了clear
刚遍历过的哈希桶里的节点。
ConcurrentHashMap提供了一个Traverser
用作只读迭代器。设计这个迭代器的难点在于,需要考虑并发的扩容,所以需要考虑当遍历遇到ForwardingNode
时,应该怎么处理。
原理很简单,当遇到ForwardingNode
时,保存当前遍历信息放入一个模拟的栈中,然后在ForwardingNode.nextTable
上继续遍历。当然,在nextTable上,我们只会遍历两个桶:原哈希桶对应的低桶和高桶。如果在返回原table之前,又遇到了ForwardingNode
,则继续深入,重复这样的步骤。返回上一个table之前,需要遍历完当前table的两个桶,才能返回。
因为跳转到新的table上去就像函数调用一样,所以模仿函数调用,作者设计了一个模拟的栈TableStack
。它是一个链栈,入栈使用头插法,出栈使用头删法。
原理图如上,这个过程很清晰易于理解,但代码实现看起来稍微复杂。
//进入下一个table前,用来保存当前table的遍历信息的快照
static final class TableStack<K,V> {
int length;//当前遍历table的大小
int index;//当前遍历索引
Node<K,V>[] tab;//当前遍历table
TableStack<K,V> next;//栈的下一个元素
}
static class Traverser<K,V> {
Node<K,V>[] tab; // 当前遍历的table
Node<K,V> next; // 下一个将返回的Node,如果为null,需要去获取
TableStack<K,V> stack, spare; // stack是模拟的栈,spare是保存出栈元素以备用(相当于回收站)
int index; // 通过index获取下一个Node
int baseIndex; // 最开始的table的遍历索引,在跳转table后index会变化,但baseIndex会一直保存的
int baseLimit; // 最开始的table的遍历索引限制
final int baseSize; // 最开始的table的大小
Traverser(Node<K,V>[] tab, int size, int index, int limit) {
this.tab = tab;
this.baseSize = size;
this.baseIndex = this.index = index;//如果没有发生跳转table,那么这二者总是保持一致的
this.baseLimit = limit;
this.next = null;
}
/**
* 获得下一个节点,如果到头了,就返回null
*/
final Node<K,V> advance() {
Node<K,V> e;
if ((e = next) != null)//首先从next获得,一般情况下next已经准备好
e = e.next;
for (;;) {
Node<K,V>[] t; int i, n;
if (e != null)
return next = e;//准备好next
//如果next没有准备好,则需要通过index获得了
//如果遍历到头了
if (baseIndex >= baseLimit || (t = tab) == null ||//超过了限制
(n = t.length) <= (i = index) || i < 0)//索引到达size
return next = null;
//如果是特殊节点
if ((e = tabAt(t, i)) != null && e.hash < 0) {
if (e instanceof ForwardingNode) {//需要转发table了
tab = ((ForwardingNode<K,V>)e).nextTable;//更新tab为新table
e = null;//下一个循环需要重新获取
pushState(t, i, n);//入栈,保存旧table的当前遍历信息
continue;//下一次循环,直接获得新table的低桶的头节点
}
else if (e instanceof TreeBin)//如果是红黑树
e = ((TreeBin<K,V>)e).first;//当作单链表使用
else//保留节点,没有数据
e = null;//下一个循环需要重新获取
}
if (stack != null)//如果栈不为空
//1. 要么还在当前新table,但索引移动到高桶索引
//2. 要么回到旧table,索引也根据快照恢复
recoverState(n);
//如果栈为空
//如果n为最开始的table的大小,那么无论i是什么,条件肯定成立
//如果n已经是新table的大小了,那么无论i是什么,条件不成立,让i变成高桶索引
//(这其实不可能,实际过程中,索引变成高桶索引的任务,都交给了recoverState)
else if ((index = i + baseSize) >= n)
//如果是最开始的table,需要恢复索引为正常索引+1
index = ++baseIndex; // visit upper slots if present
}
}
/**
* 入栈操作,保存当前遍历信息
*/
private void pushState(Node<K,V>[] t, int i, int n) {
TableStack<K,V> s = spare; // 如果spare不为null
if (s != null)
spare = s.next;
else
s = new TableStack<K,V>();
s.tab = t;
s.length = n;
s.index = i;
//这两句以头插法,入栈
s.next = stack;
stack = s;//让栈指针指向栈顶
}
/**
* 1. 将索引成员移动到高桶索引
* 2. 高桶已经遍历完,此函数将根据栈顶元素回到上一个table,
* 如果发现上一个table也遍历完了(恢复快照后发现索引处于高桶索引),
* 将回到上上个table
*/
private void recoverState(int n) {//n为当前遍历的table的大小
TableStack<K,V> s; int len;
//第二个条件不成立,此函数执行的是1过程
//第二个条件成立,此函数执行的是2过程。如果发现上一个table也遍历完了,循环将再次进入
while ((s = stack) != null && (index += (len = s.length)) >= n) {
//将栈顶元素的信息赋值给成员
n = len;//局部变量n恢复成更小table的大小
index = s.index;
tab = s.tab;
s.tab = null;
TableStack<K,V> next = s.next;
s.next = spare; // 将清空的s头插法放到spare里备用
stack = next;//栈顶指针下移,相当于出栈
spare = s;//s现在是spare的头指针
}
//如果stack为null,说明当前遍历已经回到了最开始的table,需要根据baseSize恢复index
if (s == null && (index += baseSize) >= n)
index = ++baseIndex;
}
}
直接看代码不好理解,所以假设遍历过程如下图。根据这个过程,我们来看Traverser的执行过程。
假设所有哈希桶都是链表结构,方便分析。其实就算是红黑树也无所谓,因为也是当作链表来使用的。
①步骤:
next
为null,索引成员(指index)为图中的index。此时有人执行了advance
,进入死循环。不会进入if (e != null) return next = e;
,然后通过index获得下一个遍历索引的头节点,发现是个ForwardingNode。pushState
后,stack保存了1倍table的执行信息快照(栈元素有一个),spare为null。continue下一次循环。②步骤:
recoverState(n)
,注意此时传入的n是2倍table(的大小,后面将忽略这半句话)。执行index += (len = s.length)) >= n
,此时s为栈顶元素的table的大小,即len为1倍table,n为2倍table,所以这个条件肯定不成立(因为index是一个小于1倍table大小),此时index变成高桶索引。后面的if (s == null && (index += baseSize) >= n)
由于短路,也不进入分支。recoverState(n)
负责将索引变成高桶索引 index+1倍table。在③步骤之前,循环将遍历完2倍table的低桶。③步骤:
next
为null,索引为 index+1倍table。发现是个ForwardingNode。pushState
后,stack保存了2倍table的执行信息快照(栈元素有两个),spare为null。continue下一次循环。④步骤:
recoverState(n)
,注意此时传入的n是4倍table。执行index += (len = s.length)) >= n
,此时s为栈顶元素的2倍table的大小,即len为2倍table,n为4倍table,注意此时索引为 index+1倍table,再加2倍table,也不可能大于4倍table大小。所以这个条件肯定不成立,此时index变成高桶索引。后面的if (s == null && (index += baseSize) >= n)
由于短路,也不进入分支。recoverState(n)
负责将索引变成高桶索引 index+3倍table。在⑤步骤之前,循环将遍历完4倍table的低桶。⑤⑥⑦步骤(相当于一次完成了):
next
为null,索引为 index+3倍table(可以看成两部分,index+1倍table 和 2倍table)。发现是个链表节点。if (stack != null) recoverState(n);
,注意此时传入的n是4倍table。执行index += (len = s.length)) >= n
,此时s为栈顶元素的2倍table的大小,即len为2倍table,n为4倍table,加完后索引为 index+5倍table,条件成立,进入循环。
n
变成2倍table。index += (len = s.length)) >= n
依旧成立,进入循环。(因为此时,2倍table的遍历也已经完成了,需要继续处理)
n
变成1倍table。if (s == null && (index += baseSize) >= n)
进入,因为此时n为1倍table,baseSize也为1倍table。此时baseIndex保存着1倍table的索引的快照,所以用它来恢复index(index = ++baseIndex
)。主要讲一下KeySet
吧,因为JUC框架中并没有一个类叫做ConcurrentHashSet,我们可以通过Set
的方式得到一个线程安全的set。它的实现其实就是依赖于一个ConcurrentHashMap,这里只要知道它是继承了Set接口即可。
public static <K> KeySetView<K,Boolean> newKeySet() {
return new KeySetView<K,Boolean>
(new ConcurrentHashMap<K,Boolean>(), Boolean.TRUE);
}
public static class KeySetView<K,V> extends CollectionView<K,V,K>
implements Set<K>, java.io.Serializable {
...
}
三种特殊节点,红黑树节点和转发节点ForwardingNode我们都讲过了。ReservationNode用作一个占位节点,在computeIfAbsent和compute函数使用中。
//1. 当map中没有包含参数key的映射,该函数才添加映射。
//2. 添加失败时,返回已存在映射的旧value;添加成功时,返回null。
//3. 添加的映射的key已经给出,但value需要通过Function算出来。
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());//根据原始hash值,获得处理后的hash值
V val = null;
int binCount = 0;//将代表一个哈希桶内节点数
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果发现table还没初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果发现索引所在元素为null,那么就CAS该null元素
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
if (casTabAt(tab, i, null, r)) {//先把位置占住
//进入分支,说明占位成功
binCount = 1;
Node<K,V> node = null;
try {
if ((val = mappingFunction.apply(key)) != null)
node = new Node<K,V>(h, key, val, null);
} finally {
setTabAt(tab, i, node);
//不管Function执行结果如何,都把这个执行结果赋值过去。node可能是null
}
}
}
if (binCount != 0)
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
boolean added = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek; V ev;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
val = e.val;//如果找到了包含key的映射,那么不执行函数,返回旧value
//因为这属于添加失败
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {//遍历到最后,都没有找到包含key的映射
if ((val = mappingFunction.apply(key)) != null) {
added = true;//添加成功
pred.next = new Node<K,V>(h, key, val, null);
}
break;
}
}
}
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
//如果找到了包含key的映射,那么不执行函数,返回旧value
(p = r.findTreeNode(h, key, null)) != null)
val = p.val;
else if ((val = mappingFunction.apply(key)) != null) {
added = true;//添加成功
t.putTreeVal(h, key, val);
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (!added)//添加失败,返回旧值
return val;
break;
}
}
}
if (val != null)
addCount(1L, binCount);
return val;//添加失败时,val不为null;添加成功时,val必为null
}
该函数的实现和putVal
很像啊,因为这个函数做的其实也是插入操作嘛,自然会很像。
分析类似,看注释即可。
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (key == null || remappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
V val = null;
int delta = 0;
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) & h)) == null) {
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
if ((val = remappingFunction.apply(key, null)) != null) {
delta = 1;
node = new Node<K,V>(h, key, val, null);
}
} finally {
setTabAt(tab, i, node);//新增操作
}
}
}
if (binCount != 0)
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f, pred = null;; ++binCount) {
K ek;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {//如果找到了包括key的映射
val = remappingFunction.apply(key, e.val);
//如果算出来的value不为null
if (val != null)
e.val = val;//替换操作
//如果算出来的value为null
else {
delta = -1;
Node<K,V> en = e.next;
//删除操作
if (pred != null)
pred.next = en;
else
setTabAt(tab, i, en);
}
break;
}
pred = e;
if ((e = e.next) == null) {//如果没有找到包含key的映射
val = remappingFunction.apply(key, null);//计算新value
if (val != null) {
delta = 1;
pred.next =
new Node<K,V>(h, key, val, null);//新增操作
}
break;
}
}
}
else if (f instanceof TreeBin) {
binCount = 1;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null)
p = r.findTreeNode(h, key, null);
else
p = null;
V pv = (p == null) ? null : p.val;//如果找到了映射,保存旧值
val = remappingFunction.apply(key, pv);//根据旧值计算出新值
//新值不为null
if (val != null) {
if (p != null)//如果映射被找到
p.val = val;//替换操作
else {//如果映射没被找到
delta = 1;
t.putTreeVal(h, key, val);//新增操作
}
}
//新值为null,而且映射之前被找到,执行
else if (p != null) {
delta = -1;
//删除操作
if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
break;
}
}
}
if (delta != 0)
addCount((long)delta, binCount);
return val;
}
第一步,计算出新值val
:
remappingFunction.apply(key, 旧value)
。remappingFunction.apply(key, null)
。val
都会被计算出来,并且作为返回值返回。第二步,决定要执行哪种操作。但首先得知道,ConcurrentHashMap是不允许value为null的,但是新值val
计算出来却可以为null。
val
计算出来不为null,那么执行替换操作。val
计算出来为null,那么执行删除操作。val
计算出来不为null,那么执行新增操作。val
计算出来为null,那么啥也不干。占位以后,别的插入操作就无法插入了,因为插入操作还没法获得到锁。
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);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
private static final long ABASE;
private static final int ASHIFT;
static {
try {
Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak);
int scale = U.arrayIndexScale(ak);
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
} catch (Exception e) {
throw new Error(e);
}
}
ABASE
简单理解为,第一个数组元素开始存储的偏移地址。(猜想:一个数组最开始存的可能是元素类型Class
、或者数组长度,之后才开始存储第一个数组元素)Node
是引用类型,scale肯定为4即100
,它的前面还有29个0,所以ASHIFT
为2。其实ASHIFT
就是100
后面的0的个数,而一个数左移ASHIFT
位,就相当于一个数乘以 2 A S H I F T 2^{ASHIFT} 2ASHIFT。tabAt
之类的方法里,使用了(long)i << ASHIFT
来代替(long)i * 2^ASHIFT
。& (n-1)
而不是% n
,哈希值需要高位扰动,哈希桶分离分为低桶和高桶。TreeBin
封装了真正的红黑色节点TreeNode
,TreeBin
起到一个dummy node的作用,避免了红黑树平衡造成的table槽位成员发生变化。null
。TreeBin
也是总是不变的,除非哈希桶分离。baseCount
和counterCells
,它们的实现类似于LongAdder
,这比使用AtomicLong
用作计数好多了。因为很大程度减小了因CAS失败而导致的自旋。transfer
是扩容的真正实现,这一方法被设计成可以被多个线程并发执行,以加快扩容的完成。使用ForwardingNode
来连接旧table的节点和新table。Traverser
用作只读迭代器,它在遇到ForwardingNode
时进行table跳转,然后在新table上读取相应的低桶和高桶。size()
返回的大小可能已经与真实大小不一样,比如clear()
调用返回后Map中却拥有着元素。