并发编程八、J.U.C并发工具之ConcurrentHashMap
一、CHM的使用
ConcurrentHashMap是 J .U.C 包里面提供的一个线程安全并且高效的 HashMap ,所以 ConcurrentHashMap 在并发编程的场景中使用的频率比较高。
ConcurrentHashMap也是Map的派生类,所以 api 基本和 Hashmap 类似,主要就是 put 、get 这些方法,接下来基于 ConcurrentHashMap 的put和get这两个方法作为切入点来分析 ConcurrentHashMap 的源码实现
二、CHM源码分析
本章分析的ConcurrentHashMap 是基于jdk1.8的版本。
1. JDK1.7和1.8的变化
ConcurrentHashMap和HashMap的实现原理是差不多的,但是因为ConcurrentHashMap需要支持并发操作,所以在实现上要比hashmap稍微复杂一些。
在JDK 1.7 的实现上,ConrruentHashMap由一个个Segment组成,简单来说,ConcurrentHashMap是一个Segment数组,它通过继承ReentrantLock来进行加锁,通过每次锁住一个segment来保证每个segment内的操作的线程安全性从而实现全局线程安全。整个结构图如下
当每个操作分布在不同的segment上的时候,默认情况下,理论上可以同时支持16个线程的并发写入。
相比于1.7版本,1.8做了两个改进:
- 取消了segment分段设计,直接使用Node数组来保存数据,并且采用Node数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率
- 将原本数组单向链表的数据结构变更为了数组单向链表-红黑树的结构。为什么要引入红黑树呢?
在正常情况下, key hash之后如果能够很均匀的分散在数组中,那么table数组中的每个队列的长度主要为 0 或者 1. 但是实际情况下,还是会存在一些队列长度过长的情况。如果还采用单向列表方式,那么查询某个节点的时间复杂度就变为 O(n); 因此对于队列长度超过8的列表, JDK 1.8 采用了红黑树的结构,那么查询的时间复杂度就会降低到O(logN), 可以提升查找的性能;
这个结构和JDK1.8 版本中的Hashmap的实现结构基本一致,但是为了保证线程安全性,ConcurrentHashMap 的实现会稍微复杂一下。
2. 源码分析
CHM是以一个数组+单向链表/红黑树 的方式存储,并在合适的位置加锁来保证线程安全及高并发情况下的效率提升。
接下来我们从源码层面来了解一下它的原理我们基于put 和 get 方法来分析它的实现即可。
源码分析主要有一下几个场景:
a. put 放入第一个元素 : 此时数组为空需要初始化,且要考虑多线程情况下的: 并行初始化数组、并行修改数组内元素 的场景,保证线程安全;
b. put 放入key相同的元素,value需要替换
c. put 放入key不同,但是hash相同的元素, 维护于单向链表内 node.next
d. put成功后,map size需要累加调用addCount
e. 统计梳理addCount后,当元素个数大于等于阈值sizeCtl,transfer扩容阶段
* 高低位迁移 resizeStamp
* 扩容 transfer
* 辅助线程扩容 helpTransfer
f. 当链表长度大于8,且数组长度大于64,转化为红黑树
a、put 存入第一个元素
java.util.concurrent.ConcurrentHashMap#
public V put(K key, V value) {
return putVal(key, value, false);
}
transient volatile Node<K,V>[] table; // (tips: 虽然volatile修饰了数组,并不能保证数组内每个元素的可见性)
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // A01. 计算hash值
int binCount = 0; // 记录单向链表长度
for (Node<K,V>[] tab = table;;) { // tab赋值,自旋:线程竞争情况下本次处理不成功的话可以继续自旋处理
Node<K,V> f; int n, i, fh; // (n:数组长度;i:元素数组下标;fh:元素hash值;)
if (tab == null || (n = tab.length) == 0) // 如果数组为空,进行数组初始化
tab = initTable(); // A02-A14. 数组初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// A15. 下一次的自旋逻辑; i赋值: (n-1)&hash得出元素在数组中的下标; f:取得数组i下标的元素,tab[i]此时数组刚创建为null
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) // A16. 将当前key、value封装为node,CAS放入数组对应下标,至此 元素放入map成功
break; // no lock when adding to empty bin
}
...
}
addCount(1L, binCount); // A17. size+1,保证线程安全; 见后续分析
return null;
}
private static final int DEFAULT_CAPACITY = 16;
private transient volatile int sizeCtl;
// A02-A14. 数组初始化; 以t1、t2两个线程同时初始化为例
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { // 数组为空,自旋
if ((sc = sizeCtl) < 0) // A03. sizeCtl默认为0,t1、t2线程均为 false
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // A04. CAS原子操作,只能有一个线程将 sizeCtl 标志由 0 -> -1,假如t1线程修改成功,则表示t1线程抢占成功,返回true
try {
if ((tab = table) == null || tab.length == 0) { // A09. t1线程继续运行,数组为空
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 默认数组长度 DEFAULT_CAPACITY 为16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // A10. 最终,t1线程初始化了一个长度为16的数组,元素类型为 Node(Node为单向链表结构)
table = tab = nt; // A11. table赋值
sc = n - (n >>> 2); // A12. 计算下次扩容大小, n-(n>>>2) = n - n/(2^2) = n(1-1/4) = n*0.75,即下次扩容0.75倍,此时为16*0.75=12
}
} finally {
sizeCtl = sc; // A13. sizeCtl赋值为12;
}
break; // 自旋结束
}
}
return tab; // A14. 返回一个长度为16的一位数组
}
// t2线程第一次初始化失败
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { //A06. t2线程第二次自旋
if ((sc = sizeCtl) < 0) // A07. 此时 sizeCtl=-1,小于0
Thread.yield(); // lost initialization race; just spin // A08. t2线程释放时间片 ; (tips: 当t2线程重新获得时间片,大概率线程已被t1线程初始化,再次自旋时A06处判断数组不为空,直接返回当前数组table)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // A05. 由于t1线程CAS将sizeCtl改为了-1, t2线程CAS失败 返回false
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;
}
// A15. tabAt, 取得数组下标的元素;相当于 tab[i]
@SuppressWarnings("unchecked")
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);
}
// A16. 将当前key、value封装为node,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);
}
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为单向链表的结构
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
...
...
}
tabAt
方法分析
该方法获取对象中offset偏移地址对应的对象field的值。实际上这段代码的含义等价于 tab[i],但是为什么不直接使用tab[i]来计算呢?
getObjectVolatile,一旦看到 volatile 关键字,就表示可见性。因为对volatile写操作happen-before于volatile读操作,因此其他线程对table的修改均对get读取可见;虽然table数组本身是增加了volatile属性,但是volatile的数组只针对数组的引用具有volatile 的语义,而不是它的元素。所以如果有其他线程对这个数组的元素进行写操作,那么当前线程来读的时候不一定能读到最新的值。出于性能考虑,Doug Lea 直接通过Unsafe类来对table进行操作。
b. put 放入key相同的元素,value需要替换
/** 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;;) { // B01.自旋 , tab=table
Node<K,V> f; int n, i, fh; // (f:元素节点;n:数组长度;i:元素下标位置;fh:f.h,元素节点的hash值;)
if (tab == null || (n = tab.length) == 0) // B02. table 不为空, n = tab.length
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // B03. tab[i]不为空, i=元素在数组的下标, f=tab[i] 单向链表
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) // B04. fu赋值,fh=node.hash
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { // B05. f加锁,即节点加锁,保证线程安全
if (tabAt(tab, i) == f) { // 比较来保证线程安全
if (fh >= 0) {
binCount = 1; // B06. 代表当前链表长度,到这个逻辑,当前node的链表长度至少为1
for (Node<K,V> e = f;; ++binCount) { //B07. 自旋,统计链表长度 binCount
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { // B08. 节点key完全相同,则替换value
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value; // B09. 替换value
break; // B10. 退出自旋
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); // 统计 size+1,后续分析
return null;
}
c. put 放入key不同,但是hash相同的元素,单向链接next
java.util.concurrent.ConcurrentHashMap#putVal
static final int TREEIFY_THRESHOLD = 8;
/** 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;;) { // C01. tab = table
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) // C02. 非空, n = tab.length
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // C03. i为数组下标,f为table[i]不为null
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) // C04. fh = f.hash,为元素节点hash
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { //C05. 节点加锁
if (tabAt(tab, i) == f) { // 安全性判断
if (fh >= 0) {
binCount = 1; // node 链表长度统计
for (Node<K,V> e = f;; ++binCount) { // C06.自旋 ++binCount
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {// C07. hash相同,key不同,false不进入此逻辑
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) { // C08. 获得当前节点的下一节点(即tab[i],头结点的next),如果不为空,进入下次自旋(继续C06 -> C07 -不符-> C08);如果为空表示到达单向链表尾部,
pred.next = new Node<K,V>(hash, key, value, null); // C09. 入队,将当前 key、value 封装为node节点存储于单向链表尾部
break; // C10. 入队完成,退出自旋
}
}
}
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;
}
}
}
} // C11. synchronized结束,释放锁
if (binCount != 0) { // C12. 统计本次入队链表长度
if (binCount >= TREEIFY_THRESHOLD) // C13 TREEIFY_THRESHOLD为8, 链表长度如果大于等于临界值8,
treeifyBin(tab, i); // C14. 如果链表长度达到临界值8,替换为树结构,提升遍历效率
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); // 将当前ConcurrentHashMap的元素数量加1,有可能触发transfer操作(扩容)
return null;
}
d. put成功后,map size需要累加调用addCount
在putVal方法执行完成以后,会通过addCount来增加 ConcurrentHashMap 中的元素个数,并且还会可能触发扩容操作。
这里会有两个非常经典的设计1. 高并发下的扩容、2. 如何保证 addCount 的数据安全性以及性能
。
因为count++不能保证原子性,肯定不能用来统计map的数量;
CAS通过自旋来完成简单方便,但是当在并发量巨大的场景很可能会一直自旋,浪费cpu资源;
所以ConcurrentHashMap#addCount
方法内会有两种逻辑:
- 先 CAS尝试一次 baseCount+1操作,如果更改成功,表示当前没有线程竞争,且数量统计成功
- 如果首次 CAS不成功,表明有线程竞争也在修改 baseCount,这个时候不能用 baseCount累加,采用CounterCell数组来辅助记录:
java.util.concurrent.ConcurrentHashMap#addCount
private transient volatile long baseCount; // 总数值 map size
private transient volatile CounterCell[] counterCells; // counterCells数组,总数值的分值分别存在每个cell中
// addCount的时候,传递了两个参数: 分别是 1 和 binCount(链表长度)
// x 代表需要在表中增加的元素个数;check 代表是否需要进行扩容检查,大于等于0都需要检查
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null || // D01. 判断 counterCells数组是不是null:如果是null,代表不存在竞争,执行D02-baseCount累加;如果不为Null,存在竞争
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { // D02. 如果D01中为null,执行CAS操作baseCount累加:如果CAS成功则累加成功; 否则使用CountCell来记录
CounterCell a; long v; int m;
boolean uncontended = true; // D03. 是否冲突标识,默认为没有冲突
/**
*这里有几个判断
*1. cells计数表为空则直接调用fullAddCount
*2. 从计数表中随机取出一个数组的位置为空,直接调用fullAddCount
*3. 通过CAS修改CounterCell随机位置的值,如果修改失败说明出现并发情况(这里又用到了一种巧妙的方法),调用fullAndCount Random在线程并发的时候会有性能问题
* 以及可能会产生相同的随机数,ThreadLocalRandom.getProbe可以解决这个问题,并且性能要比Random高
*/
if (as == null || (m = as.length - 1) < 0 || // D04. 计数表为空则直接调用fullAddCount
(a = as[ThreadLocalRandom.getProbe() & m]) == null || // D05. 从计数表中随机取出一个数组的位置为空,直接调用fullAddCount
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // D06. 通过CAS修改CounterCell随机位置的值,如果修改失败说明出现并发情况,调用fullAddCount
fullAddCount(x, uncontended); // D08. 计数表counterCells为空时初始化,非空且存在并发时累加计数 fullAddCount
return;
}
if (check <= 1) // 链表长度小于等于1,不扩容
return;
s = sumCount(); // 最终,统计ConcurrentHashMap元素个数
}
if (check >= 0) {
// 是否扩容 见下面 transfer 逻辑
Node<K,V>[] tab, nt; int n, sc;
...
}
}
// 看到这段代码就能够明白了,CounterCell数组的每个元素,都存储一个元素个数,而实际我们调用size方法就是通过这个循环累加来得到的
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;
}
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
CounterCells解释
ConcurrentHashMap是采用 CounterCell 数组来记录元素个数的,像一般的集合记录集合大小,直接定义一个 size 的成员变量即可,当出现改变的时候只要更新这个变量就行。为什么ConcurrentHashMap要用这种形式来处理呢?
问题还是处在并发上,ConcurrentHashMap 是并发集合,如果用一个成员变量来统计元素个数的话,为了保证并发情况下共享变量的安全性,势必会需要通过加锁或者自旋来实现,如果竞争比较激烈的情况下, size 的设置上会出现比较大的冲突反而影响了性能,所以在ConcurrentHashMap 采用了分片的方法来记录大小,具体实现见下面分析:
private transient volatile int cellsBusy; // 标识当前cell数组是否在初始化、扩容、或者修改cell.value值中的CAS标志位
private transient volatile CounterCell[] counterCells; // counterCells数组,总数值的分值分别存在每个cell中
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
fullAddCount源码
fullAddCount 主要是用来初始化CounterCell,来记录元素个数,里面包含扩容,初始化等操作
private transient volatile int cellsBusy; // 标识当前cell数组是否在初始化、扩容、或者修改cell.value值中的CAS标志位
private transient volatile CounterCell[] counterCells; // counterCells数组,总数值的分值分别存在每个cell中
// D08. 计数表counterCells为空时初始化,非空且存在并发时累加计数
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) { // D09. 获取当前线程的probe的值,如果值为0,则初始化当前线程的probe的值,probe就是随机数
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true; // 由于重新生成了probe,未冲突标志位设置为true
}
boolean collide = false; // True if last slot nonempty
for (;;) { // D10. 自旋
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) { // D11. 判断 counterCells是否初始化,初始化过进入本次逻辑;先看else逻辑 -> D12-D18. 初始化counterCells
if ((a = as[(n - 1) & h]) == null) { // D19. 如果执行这个逻辑,说明之前 cell数组初始化完毕,此时随机下标cell为空
if (cellsBusy == 0) { // Try to attach new Cell cell不忙
CounterCell r = new CounterCell(x); // Optimistic create // x封装于CounterCell内
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { // 标记 cellsBusy
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r; // 放入数组内
created = true;
}
} finally {
cellsBusy = 0; // 释放 cellsBusy
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)) // D20. 随机下标 cell[i]非空, 直接CAS累加元素内value值;如果失败则继续自旋
break;
else if (counterCells != as || n >= NCPU) //如果已经有其他线程建立了新的counterCells或者CounterCells大于CPU核心数(很巧妙,线程的并发数不会超过cpu核心数)
collide = false; // At max size or stale 设置当前线程的循环失败不进行扩容
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { // D21. 进入这个步骤,说明CounterCell数组容量不够,线程竞争较大,所以先设置一个标识表示为正在扩容
try {
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1]; // 并发激烈, cell数组扩容,左移1位,长度乘2
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 && // D12. cellsBusy=0表示没有在做初始化,通过cas更新cellsbusy的值标注当前线程正在做初始化操作
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { // D13. cas将 cellsBusy设为1,保证初始化数组时的线程安全
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2]; // D14. 辅助统计总数量的 CounterCell数组默认长度为2
rs[h & 1] = new CounterCell(x); // D15. 将本次增量x封装为CounterCell,放入数组随机位置
counterCells = rs; //赋值给counterCells
init = true; // D16. 设置初始化完成标识
}
} finally {
cellsBusy = 0; // D17. CounterCell修改完毕,cellsBusy重置
}
if (init)
break; // D18. 退出自旋
}
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x)) // 并发竞争激烈,其它线程占据cell数组,直接累加在base变量中;失败的话继续自旋
break; // Fall back on using base
}
}
e. 统计梳理addCount后,当元素个数大于等于阈值sizeCtl,transfer扩容阶段
判断是否需要扩容,也就是当更新后的键值对总数baseCount >= 阈值sizeCtl时,进行refash,这里面会有两个逻辑:
- 如果当前正在处于扩容阶段,则当前线程会加入并且协助扩容
- 如果当前没有在扩容,则直接触发扩容操作
private transient volatile int sizeCtl; // 在第一次数组初始化16为长度时, sizeCtl被初始化为 16*0.75 = 12
transient volatile Node<K,V>[] table; // 单例链表的数组
private static int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// addCount的时候,传递了两个参数: 分别是 1 和 b inCount(链表长度)
// x 代表需要在表中增加的元素个数;check 代表是否需要进行扩容检查,大于等于0都需要检查
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;
...
...
s = sumCount();
}
if (check >= 0) { // 如果binCount>=0,标识需要检查扩容
Node<K,V>[] tab, nt; int n, sc;
//s标识集合大小,如果集合大小大于或等于扩容阈值(默认值的0.75倍); 且table不为空并且table的长度小于最大容量; 表示满足扩容条件
while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { // E01. 当前map数量s满足扩容阈值&&数组不为null&&数组长度小于最大容量
int rs = resizeStamp(n); // E02. 这里是生成一个唯一的扩容戳 ?? 高低位迁移
if (sc < 0) { // E03. 如果sc<0,表明当前有线程正在扩容
//这5个条件只要有一个条件为true,说明当前线程不能帮助进行此次的扩容,直接跳出循环
//sc >>> RESIZE_STAMP_SHIFT!=rs 表示比较高位RESIZE_STAMP_BITS生成戳和rs是否相等,不同说明数组长度已经改变,此次不能扩容
//sc=rs+1 表示扩容结束
//sc==rs+MAX_RESIZERS 表示帮助线程线程已经达到最大值了
//nt=nextTable -> 表示扩容已经结束
//transferIndex<=0 表示所有的transfer任务都被领取完了,没有剩余的hash桶来给自己自己好这个线程来做transfer
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); // E06. 当正在扩容,满足条件的线程协助扩容
}
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) // E04. 如果当前没有在扩容,那么rs肯定是一个正数,通过rs<<RESIZE_STAMP_SHIFT将sc设置为一个负数,+2表示有一个线程在执行扩容
transfer(tab, null); // E05. 第一个线程扩容
s = sumCount();
}
}
}
/**
* 不得不慨叹高低位迁移设计的精妙,仅仅一个变量记录了扩容前数据长度、并发扩容的线程数、是否有线程正在扩容,而且性能还高
*/
高低位迁移 resizeStamp
resizeStamp 用来生成一个和扩容有关的扩容戳,我们基于它的实现来分析:
首先根据扩容前数组长度来生产扩容戳;将扩容戳左移16位,低16位变高16位,然后高16位代表扩容戳,低16位代表并行扩容线程数;
java.util.concurrent.ConcurrentHashMap#resizeStamp
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
java.lang.Integer#numberOfLeadingZeros
public static int numberOfLeadingZeros(int i) {
// HD, Figure 5-6
if (i == 0)
return 32;
int n = 1;
if (i >>> 16 == 0) { n += 16; i <<= 16; }
if (i >>> 24 == 0) { n += 8; i <<= 8; }
if (i >>> 28 == 0) { n += 4; i <<= 4; }
if (i >>> 30 == 0) { n += 2; i <<= 2; }
n -= i >>> 31;
return n;
}
private final void addCount(long x, int check) {
...
n = tab.length
...
int rs = resizeStamp(n);
...
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
...
}
Integer.numberOfLeadingZeros(n)
这个方法是返回无符号整数n最高位非0位前面的0的个数;
比如10的二进制是[0000 0000 0000 0000 0000 0000 0000 1010]那么这个方法返回的值就是28。
根据 resizeStamp 的运算逻辑,我们来推演一下,假如n=16 ,那么resizeStamp(16)=32796转化为二进制是 [0000 0000 0000 0000 1000 0000 0001 1100];接着再来看,当第一个线程尝试进行扩容的时候,会执行下面这段代码
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
rs << 16, rs左移16位,相当于原本的二进制低位变成了高位 [1000 0000 0001 1100 0000 0000 0000 0000],高低位迁移;
然后再+2,[1000 0000 0001 1100 0000 0000 0000 0000] + 10 = [1000 0000 0001 1100 0000 0000 0000 0010]。
高16位代表扩容的标记、低16位代表并行扩容的线程数:
高RESIZE_STAMP_BITS位 | 低RESIZE_STAMP_SHIFT位 |
---|---|
扩容标记 | 并行扩容线程数 |
这样来存储有什么好处呢?
- 首先在 CHM 中是支持并发扩容的,也就是说如果当前的数组需要进行扩容操作,可以由多个线程来共同负责,第一个线程开始扩容中,后续线程会协助扩容,这块后续会分析;
- 可以保证每次扩容都生成唯一的生成戳,每次新的扩容,都有一个不同的 n ,这个生成戳就是根据 n 来计算出来的一个数字, n不同,这个数字也不同;
第一个线程尝试扩容的时候,为什么是2?
因为1表示初始化,2表示一个线程在执行扩容,而且对sizeCtl的操作都是基于位运算的,所以不会关心它本身的数值是多少,只关心它在二进制上的数值,而sc+1会在低16位上加1。
扩容 transfer
扩容是ConcurrentHashMap的精华之一, 扩容操作的核心在于数据的转移,在单线程环境下数据的转移很简单,无非就是把旧数组中的数据迁移到新的数组。但是这在多线程环境下在扩容的时候其他线程也可能正在添加元素,这时又触发了扩容怎么办?
可能大家想到的第一个解决方案是加互斥锁,把转移过程锁住,虽然是可行的解决方案,但是会带来较大的性能开销。因为互斥锁会导致 所有访问临界区的线程陷入到阻塞状态,持有锁的线程耗时越长,其他竞争线程就会一直被阻塞,导致吞吐量较低。而且还可能导致死锁。
而ConcurrentHashMap并没有直接加锁,而是采用CAS实现无锁的并发同步策略,最精华的部分是它可以利用多线程来进行协同扩容简单来说,它把Node数组当作多个线程之间共享的任务队列,然后通过维护一个指针来划分每个线程锁负责的区间,每个线程通过区间逆向遍历来实现扩容,一个已经迁移完的bucket会被替换为一个 ForwardingNode 节点,标记当前 bucket 已经被其他线程 迁移完了。
接下来分析一下它的源码实现:
1、 fwd 这个类是个标识类,用于指向新表用的,其他线程遇到这个类会主动跳过这个类,因为这个类要么就是扩容迁移正在进行,要么就是已经完成扩容迁移,也就是这个类要保证线程安全,再进行操作。
2、 advance 这个变量是用于提示代码是否进行推进处理,也就是当前桶处理完,处理下一个桶的标识
3、 finishing 这个变量用于提示扩容是否结束用的
以第一次线程扩容为例: transfer(tab, null);
接上面 E05步骤: X01-X0n
java.util.concurrent.ConcurrentHashMap#transfer
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
/** Number of CPUS, to place bounds on some sizings */
static final int NCPU = Runtime.getRuntime().availableProcessors();
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile int sizeCtl; // 在第一次数组初始化16为长度时,sizeCtl被初始化为 16*0.75 = 12;在第一次扩容时,高低位迁移将至改为了负数
private transient volatile int transferIndex;
private static final int MIN_TRANSFER_STRIDE = 16;
// E05. 第一个线程扩容 以 X01步骤->推导
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//将 (n>>>3相当于 n/8) 然后除以 CPU核心数。如果得到的结果小于 16,那么就使用 16
// 这里的目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象,如果桶较少的话,默认一个 CPU(一个线程)处理 16 个桶,也就是长度为16的时候,扩容的时候只会有一个线程来扩容
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) // X01. 根据机器核心数判断,单核的话取16,多核的话以 数组长度/2^3/NCPU 和 16 取较大的值
stride = MIN_TRANSFER_STRIDE; // subdivide range // X02. 此时数组长度为16, 16/8/NCPU一定小于16, stride = 16
if (nextTab == null) { // X03. initiating nextTab未初始化,nextTab是用来扩容的node数组
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // X04. 新建一个n<<1原始table大小的nextTab,也就是32
nextTab = nt; // X05. 赋值给nextTab
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE; // 异常的话直接将扩容阈提升至nax
return;
}
nextTable = nextTab; // X06. 更新成员变量 nextTable
transferIndex = n; // X07. 更新转移下标,表示转移时的下标,此时为16
}
int nextn = nextTab.length; // X08. 新的tab的长度 32
// 创建一个 fwd 节点,表示一个正在被迁移的Node,并且它的hash值为-1(MOVED),也就是前面我们在讲putval方法的时候,会有一个判断MOVED的逻辑。
// 它的作用是用来占位,表示原数组中位置i处的节点完成迁移以后,就会在i位置设置一个fwd来告诉其他线程这个位置已经处理过了,
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // X09. fwd 代表一个正在迁移的Node,hash值为-1(MOVED),内部存储新的表,辅助扩容的线程可以获取
// 首次推进为 true,如果等于 true,说明需要再次推进一个下标(i--),反之,如果是 false,那么就不能推进下标,需要将当前的下标处理完毕才能继续推进
boolean advance = true; // X10. 推进标识 true
// 判断是否已经扩容完成,完成就return,退出循环
boolean finishing = false; // X11. 扩容完成标识 false
// 通过for自循环处理每个槽位中的链表元素,默认advace为真,通过CAS设置transferIndex属性值,并初始化i和bound值,i指当前处理的槽位序号,bound指需要处理的槽位边界,先处理槽位15的节点;
for (int i = 0, bound = 0;;) { // X12. 自旋, i代表槽位序号, bound代表槽位边界
// 这个循环使用CAS不断尝试为当前线程分配任务,直到分配成功或任务队列已经被全部分配完毕
// 如果当前线程已经被分配过bucket区域,那么会通过--i指向下一个待处理bucket然后退出该循环
Node<K,V> f; int fh;
while (advance) { // X13. while(true)自旋开始 // X20. while(false)自旋结束 // X26. 在X25处节点hash为-1,已被迁移,继续推进
int nextIndex, nextBound;
// --i表示下一个待处理的bucket,如果它>=bound,表示当前线程已经分配过bucket区域
if (--i >= bound || finishing) // X14. i此时为0,不进入 // X27. --i为 14,因为此时数组位0-15,15迁移后,自旋迁移下标为14的位置
advance = false; // X28. --i为14,推进下一节点迁移,while自旋退出
else if ((nextIndex = transferIndex) <= 0) { // 表示所有bucket已经被分配完毕 X15. 此时 nextIndex=transferIndex=16 不进入
i = -1;
advance = false;
}
// 通过cas来修改TRANSFERINDEX,为当前线程分配任务,处理的节点区间为(nextBound,nextIndex)->(0,15)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) { // X16. 此时 nextIndex、stride都是16,将nextBound赋值0, CAS将transferIndex改为0
bound = nextBound; // X17. bound = 0
i = nextIndex - 1; // X18. i = 16-1 为15
advance = false; // X19. advance置为false,不需推进,进入下次自旋,X20处判断自旋结束
}
}
//i<0说明已经遍历完旧的数组,也就是当前线程已经处理完所有负责的bucket
if (i < 0 || i >= n || i + n >= nextn) { // X21. 此时 i是15,n是16, nexttn是32 不满足条件,不进入 // X35. 16位长度数组迁移完毕,i=-1
int sc;
if (finishing) { // 如果完成了扩容 // X40. 扩容结束啦
nextTable = null; // gc
table = nextTab; // X41. 新表替换
sizeCtl = (n << 1) - (n >>> 1); // X42. 更新阈值 (16*2-16/2)=24; n*(2-0.5) = n*1.5扩容阈
return;
}
// sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
// 然后,每增加一个线程参与迁移就会将 sizeCtl 加 1,
// 这里使用 CAS 操作对 sizeCtl 的低16位进行减 1,代表做完了属于自己的任务
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // X36. 当前线程迁移完毕,sizeCtl中高位代表扩容戳,低位代表当前扩容线程数,一个线程迁移完成需要-1
// 第一个扩容的线程,执行transfer方法之前,会设置 sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2) 后续帮其扩容的线程,
// 执行transfer方法之前,会设置 sizeCtl = sizeCtl+1 每一个退出transfer的方法的线程,退出之前,会设置 sizeCtl = sizeCtl-1
// 那么最后一个线程退出时:必然有 sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
// 如果 sc - 2 不等于标识符左移 16 位。如果他们相等了,说明没有线程在帮助他们扩容了。也就是说,扩容结束了。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) // X37. 如果回归扩容戳相等, 表明全部线程迁移完毕
return;
// 如果相等,扩容结束了,更新 finising 变量
finishing = advance = true; // X38. 全部线程迁移完毕, finishing扩容结束标识为true
// 再次循环检查一下整张表
i = n; // recheck before commit // X39. i 重置为16,再重复检查一遍 , 16次循环至X24处判断逻辑?? , 16次检查后至X40 扩容结束
}
}
//如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode '空节点', hash -1
else if ((f = tabAt(tab, i)) == null) // X22. 如果数组下标i处为null,不需要迁移; f赋值为i下标的节点
advance = casTabAt(tab, i, null, fwd); // X23. CAS将原表i处进行标记,为fwd节点 ,代表已经迁移过,之后从X12处进入下次自旋
else if ((fh = f.hash) == MOVED) // X24. fh为 X22处节点的hash值, 如果这个节点被别的线程迁移过,hash=-1
advance = true; // already processed // X25. advance重置为true,代表推进下次迁移,进入下次X13处while内自旋,
else {
// 对数组该节点位置加锁,开始处理数组该位置的迁移工作
synchronized (f) { // X29. 节点不为空,加锁,开始迁移,
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn; // ln(low) 表示低位, hn(hign) 表示高位 接下来这段代码的作用是把链表拆分成两部分, 0 在低位, 1 在高位
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) { // 遍历当前bucket的链表,目的是尽量重用Node链表尾部的一部分
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) { // 如果最后更新的runBit是0,设置低位节点
ln = lastRun;
hn = null;
}
else { // 否则,设置高位节点
hn = lastRun;
ln = null;
}
// 构造高位以及低位的链表 -> 将原链表分为两种 高低位链表, 高位链表迁移至扩容数组位,低位链表位置不变
for (Node<K,V> p = f; p != lastRun; p = p.next) { // X30. 链表迁移, 将原链表内节点hash值&原数组长度,来划分为高低位两类,
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); // X31. 低位链表位置不变,数组下标仍为 i ,迁移至新表 nextTab
setTabAt(nextTab, i + n, hn); // X32. 高位链表迁移至扩容数组位,数组下标 i+n,迁移至新表 nextTab
setTabAt(tab, i, fwd); // X33. 原表的小标 i 位替换为 fwd,则为已替换
advance = true; // x34. 继续推进 X13处while自旋 重复此逻辑直至while循环内i小于0,进入 X35
}
else if (f instanceof TreeBin) { // TODO 红黑树的扩容部分
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
扩容图解
ConcurrentHashMap 支持并发扩容,实现方式是,把 Node 数组进行拆分,让每个线程处理自己的区域,假设 table 数组总长度是 64,默认情况下,那么每个线程可以分到 16 个 bucket。然后每个线程处理的范围,按照倒序来做迁移通过 for 自循环处理每个槽位中的链表元素,默认 advace 为真,通过 CAS 设置 transferIndex 属性值,并初始化 i 和 bound 值,i 指当前处理的槽位序号,bound 指需要处理的槽位边界,先处理槽位 31 的节点; (bound,i) =(16,31) 从 31 的位置往前推动。
假设这个时候 ThreadA 在进行 transfer,那么逻辑图表示如下
在当前假设条件下,槽位 15 中没有节点,则通过 CAS 插入在第二步中初始化的 ForwardingNode 节点,用于告诉其它线程该槽位已经处理过了;
sizeCtl 扩容退出机制
E:.../java/util/concurrent/ConcurrentHashMap.java:2414
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
}
每存在一个线程执行完扩容操作,就通过 cas 执行 sc-1。接着判断(sc-2) !=resizeStamp(n) << RESIZE_STAMP_SHIFT ; 如果相等,表示当前为整个扩容操作的 最后一个线程,那么意味着整个扩容操作就结束了;如果不相等,说明还得继续这么做的目的,一方面是防止不同扩容之间出现相同的 sizeCtl,另外一方面,还可以避免 sizeCtl 的 ABA 问题导致的扩容重叠的情况
高低位原理分析
ConcurrentHashMap 在做链表迁移时,会用高低位来实现,这里有两个问题要分析一下
- 如何实现高低位链表的区分
假如我们有这样一个队列
第 14 个槽位插入新节点之后,链表元素个数已经达到了 8,且数组长度为 16,优先通过扩容来缓解链表过长的问题,扩容这块的图解稍后再分析,先分析高低位扩容的原理:
假如当前线程正在处理槽位为 14 的节点,它是一个链表结构,在代码中,首先定义两个变量节点 ln 和 hn,实际就是 lowNode 和 HighNode,分别保存 hash 值的第 x 位为 0 和不等于 0 的节点,通过 fn&n 可以把这个链表中的元素分为两类,A 类是 hash 值的第 X 位为 0,B 类是 hash 值的第 x 位为不等于 0(至于为什么要这么区分,稍后分析),并且通过 lastRun 记录最后要处理的节点。最终要达到的目的是,A 类的链表保持位置不动,B 类的链表为 14+16(扩容增加的长度)=30,我们把 14 槽位的链表单独伶出来,我们用蓝色表示 fn&n=0 的节点,假如链表的分类是这样
E:/.../java/util/concurrent/ConcurrentHashMap.java:2426
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;
}
通过上面这段代码遍历,会记录 runBit 以及 lastRun,按照上面这个结构,那么 runBit 应该是蓝色节点,lastRun 应该是第 6 个节点接着,之后进行遍历,生成 ln 链以及 hn 链
接着,通过 CAS 操作,把 hn 链放在 i+n 也就是 14+16 的位置,ln 链保持原来的位置不动。并且设置当前节点为 fwd,表示已经被当前线程迁移完了,迁移完成以后的数据分布如下
为什么要做高低位的划分
要想了解这么设计的目的,我们需要从 ConcurrentHashMap 的根据下标获取对象的算法来看:
E:/.../java/util/concurrent/ConcurrentHashMap.java:1018
((f = tabAt(tab, i = (n - 1) & hash)) == null)
通过(n-1) & hash 来获得在 table 中的数组下标来获取节点数据,【&运算是二进制运算符,1&1=1,其他都为 0】。
假设我们的 table 长度是 16, 二进制是【0001 0000】,减一以后的二进制是 【0000 1111】假如某个 key 的 hash 值=9,对应的二进制是【0000 1001】,那么按照(n-1) & hash 的算法 0000 1111 & 0000 1001 =0000 1001 , 运算结果是 9当我们扩容以后,16 变成了 32,那么(n-1)的二进制是 【0001 1111】仍然以 hash 值=9 的二进制计算为例 0001 1111 & 0000 1001 =0000 1001 ,运算结果仍然是 9。
我们换一个数字,假如某个 key 的 hash 值是 20,对应的二进制是【0001 0100】,仍然按照(n-1) & hash算法,分别在 16 为长度和 32 位长度下的计算结果
16 位: 0000 1111 & 0001 0100 = 0000 0100
32 位: 0001 1111 & 0001 0100 = 0001 0100
从结果来看,同样一个 hash 值,在扩容前和扩容之后,得到的下标位置是不一样的,这种情况当然是不允许出现的,所以在扩容的时候就需要考虑,而使用高低位的迁移方式,就是解决这个问题.
大家可以看到,16 位的结果到 32 位的结果,正好增加了 16.
比如 20 & 15=4 、20 & 31=20 ; 4-20 =16
比如 60 & 15=12 、60 & 31=28; 12-28=16
所以对于高位,直接增加扩容的长度,当下次 hash 获取数组位置的时候,可以直接定位到对应的位置。这个地方又是一个很巧妙的设计,直接通过高低位分类以后,就使得不需要在每次扩容的时候来重新计算 hash,极大提升了效率。
辅助线程扩容 helpTransfer
java.util.concurrent.ConcurrentHashMap#putVal
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) // 当节点hash为MOVED,则为 fw节点,表示有线程正在扩容
tab = helpTransfer(tab, f); // 辅助线程扩容
else {
...
...
}
...
...
}
}
// 辅助扩容
java.util.concurrent.ConcurrentHashMap#helpTransfer
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 判断此时是否仍然在执行扩容,nextTab=null的时候说明扩容已经结束了
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length); // 根据数组长度生产扩容戳
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) { // 阈值sizeCtl在结束扩容后会设置为n*1.5;在开始扩容时会高低位迁移之后置为负数;所以此处阈值sizeCtl小于0表示正在扩容
//下面部分的整个代码表示扩容结束,直接退出循环
//transferIndex<=0表示所有的Node都已经分配了线程
//sc=rs+MAX_RESIZERS 表示扩容线程数达到最大扩容线程数
//sc >>> RESIZE_STAMP_SHIFT !=rs, 如果在同一轮扩容中,那么sc无符号右移比较高位和rs的值,那么应该是相等的。如果不相等,说明扩容结束了
//sc==rs+1 表示扩容结束
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break; // 跳出循环
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { // sc低16位代表扩容线程数,sc+1
transfer(tab, nextTab); // 线程开始辅助扩容
break; // 扩容结束,跳出循环
}
}
return nextTab;
}
return table;
}
f. 当链表长度大于8,且数组长度大于64,转化为红黑树
判断链表的长度是否已经达到临界值8. 如果达到了临界值,这个时候会根据当前数组的长度来决定是扩容还是将链表转化为红黑树。
也就是说如果当前数组的长度小于64,就会先扩容。否则,会把当前链表转化为红黑树。
java.util.concurrent.ConcurrentHashMap#putVal
static final int TREEIFY_THRESHOLD = 8;
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
...
if (binCount != 0) { // F01. 说明在做链表操作
if (binCount >= TREEIFY_THRESHOLD) // F02. 如果链表长度已经达到临界值8 就需要把链表转换为树结构
treeifyBin(tab, i); // F03. 转为红黑树
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
java.util.concurrent.ConcurrentHashMap#treeifyBin
static final int MIN_TREEIFY_CAPACITY = 64;
// F03. 转为红黑树
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) // tab长度是否小于64,小于64则扩容数组
tryPresize(n << 1); // 小于64扩容数组
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { // F04. tab长度大于64,转为红黑树存储
synchronized (b) { // F05. 加锁,线程安全的将当前节点转为红黑树
if (tabAt(tab, index) == b) { // 再一次检查
TreeNode<K,V> hd = null, tl = null; // head、tail
for (Node<K,V> e = b; e != null; e = e.next) { // F06. 遍历单向链表 next
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null); // F07. 将key.vale重新封装为 treeNode节点类型
if ((p.prev = tl) == null) // F08. 绑定节点之前关系, prev、next
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd)); // F09. 下标位 替换为 树节点
}
}
}
}
}
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
...
...
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}