为什么要用CHM?
在多线程的情况下,hashmap够用吗?翻看源码,发现它的put方法中插入之前,都会对插入的位置进行散列位与计算,此时存在两个线程,一个线程判断位置为空,时间片用玩还未插入,另一个线程则也会在相同位置判断为空,于是他们的插入操作就会造成数据的覆盖。
//散列表下标与传入的key取得的散列值做位与计算,从而取得新的下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
于是JDK1.0就存在的,Hashtable就成为了候选,然而直接无脑用synchronized重量级锁锁方法,是否“过于”安全,就连简单的读取操作,互不影响,还不能同时进行实在是过于影响效率。
public synchronized V put(K key, V value)
public synchronized V get(Object key)
JDK1.2提供的SynchronizedMap方法也是线程安全的,单也是"过于"安全的做法,直接在hashmap头上套一把对象锁,实在是锁向披靡。
Collections.synchronizedMap(new HashMap<>());
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
于是到了JDK1.5之后,Doug Lea为我们提供了全新的选择,它就是CHM(ConcurrentHashMap),而今比较经典的两个版本就是ConcurrentHashMap 1.7和1.8这两个版本:
JDK1.7 中 ConcurrentHashMap 采用的方案,被叫做 锁分段技术,每个部分就是一个 Segment(段)。但是,在JDK1.8中,完全重构了,采用的是 Synchronized + CAS ,把锁的粒度进一步降低,而放弃了 Segment 分段。(Synchronized升级后,采用锁升级也一定程度上提高了并发效率)
CHM 1.7初探
在 JDK1.7中,本质上还是采用链表+数组的形式存储键值对的。原来的整个 table 划分为 n 个 Segment,每个 Segment 里边是由 HashEntry 组成的数组,HashEntry里又可以形成链表。
当对某个 Segment 加锁时,我们要做的就是尽可能的让元素均匀的分布在不同的 Segment中。最理想的状态是,所有执行的线程操作的元素都是不同的 Segment,这样就可以降低锁的竞争。
//Segment 对象,继承自 ReentrantLock 可重入锁。内部的属性和方法和 HashMap类似,只是多了一些拓展功能。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//这是在 scanAndLockForPut 方法中用到的一个参数,用于计算最大重试次数
//获取当前可用的处理器的数量,若大于1,则返回64,否则返回1。
static final int MAX_SCAN_RETRIES =Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
//用于表示每个Segment中的 table,是一个用HashEntry组成的数组。
transient volatile HashEntry<K,V>[] table;
//Segment中的元素个数,每个Segment单独计数(下边的几个参数同样的都是单独计数)
transient int count;
//每次 table 结构修改时,如put,remove等,此变量都会自增
transient int modCount;
//当前Segment扩容的阈值,同HashMap计算方法一样也是容量乘以加载因子
//需要知道的是,每个Segment都是单独处理扩容的,互相之间不会产生影响
transient int threshold;
//加载因子
final float loadFactor;
}
// HashEntry,存在于每个Segment中,它就类似于HashMap中的Node,用于存储键值对的具体数据和维护单向链表的关系
static final class HashEntry<K,V> {
//每个key通过哈希运算后的结果,用的是 Wang/Jenkins hash 的变种算法,此处不细讲,感兴趣的可自行查阅相关资料
final int hash;
final K key;
//value和next都用 volatile 修饰,用于保证内存可见性和禁止指令重排序
volatile V value;
//指向下一个节点
volatile HashEntry<K,V> next;
}
CHM 1.8 源码解析
在 CHM 1.8 中,底层存储结构和 1.8 的 HashMap 是一样的,都是数组+链表+红黑树。引入了同步锁,在更细粒度的代码层面上,同步锁已经可以媲美 Lock 锁了。
sizeCtl的含义
private transient volatile int sizeCtl;
sizeCtl = 0, 代表数组未初始化, 且数组的初始容量为16
sizeCtl > 0,如果数组末初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值(数组的初始容量*0.75)
sizeCtl < 0,并且不是-1,表示数组正在扩容,-(1 +n) 表示此时有n个线程正在共同完成数组的扩容操作
sizeCtl = -1,表示数组正在进行初始化
必经之法
put方法
-
新创建的CHM首次调用put方法,桶(数组)为空,会对CHM进行初始化,初始化是以CAS加自旋的方式进行的。
-
桶(数组)不为空,则CHM初始化已完成,须先判断桶位的元素是否为空,如果为空才以CAS加自旋的方式进行元素的添加。如果桶(数组)属于扩容状态,则帮助桶扩容。
-
否则该桶位的元素存在元素,先给该桶位加同步锁,之后给该桶位进行元素的添加,添加的形式可能以链表形式也可能以红黑树的形式。
-
添加完成之后,先判断桶位节点数是否大于等于 8,如果大于等于 8则先判断桶的容量是否小于 64,如果小于64,则先进行扩容,直到达到树化最小容量64,再进行树化;否则直接树化。
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//与hashmap不同,CHM的key值和value值都不能为空
if (key == null || value == null) throw new NullPointerException();
//spread方法与HashMap 的hash方法大同小异
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();
//取当前桶位下标元素赋值给f,并判空
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
}
//如果该桶位元素hash值为MOVED,则表示该节点正在扩容,当前线程将会对它进行协助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//保证该桶位插入的操作线程安全
synchronized (f) {
//存在线程对该桶位树化或扩容导致该桶位元素改变,须校验
if (tabAt(tab, i) == f) {
//fh >= 0则表示该桶位下是一个链表结构
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, 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) {
//如果节点个数大于等于 8,则转化为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
spread 方法
0x7fffffff ,二进制为 0111 1111 1111 1111 1111 1111 1111 1111 。
hash值除了做了高低位异或运算,还多了一步,与 HASH_BITS 做与运算,保证最高位总是0,返回的hash值始终为正数。以便后续添加元素时判断该节点类型是链表还是红黑树。
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
initTable 方法
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
//sizeCtl<0,代表存在线程正在初始化或扩容,当前线程让出对CPU的使用而自旋
Thread.yield(); // lost initialization race; just spin
//利用Unsafe的CAS方法将sizeCtl赋值为-1,表示正在初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//double check 二次判空验证,防止其他线程已经初始化,当前线程重复初始化而使数据覆盖
if ((tab = table) == null || tab.length == 0) {
//初始化数组的初始容量,传sizeCtl值则取其指定大小,否则取默认初始容量 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//同 (1-1/4)n -> 0.75n
sc = n - (n >>> 2);
}
} finally {
//将扩容阈值存入sizeCtl中
sizeCtl = sc;
}
break;
}
}
return tab;
}
tabAt 方法
使用Unsaf.getObjectVolatile方法,是为了在不加锁的情况下保证数组中元素的一致性,数据从主内存读到线程的工作内存,保证线程间的可见性。
private static final long ABASE;
//table数组的第一个元素的起始地址
private static final int ASHIFT;
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
//(long)i << ASHIFT) + ABASE 获取下标i元素的偏移地址
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
casTabAt 方法
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);
}
treeifyBin 方法
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));
}
}
}
}
}
addCount 方法
put方法插入成功之后,会调用addCount方法,来统计桶位元素个数
如果在 HashMap 的 size 记数基础上,采用 volatile + CAS,也可以实现线程安全的记数,但高并发的状况下,同一时间操作size这个变量会造成严重的竞争。CHM进行了优化处理,将本应竞争的线程分散到CounterCell对象中,最后将CounterCell对象中的baseCount记数相加即可。
//线程被分配到的格子
@sun.misc.Contended static final class CounterCell {
//此格子内记录的 value 值
volatile long value;
CounterCell(long x) { value = x; }
}
//用来存储线程和线程生成的随机数的对应关系
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
// x为1,check代表链表上的元素个数
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//此处要进入if有两种情况
//1.数组不为空,说明数组已经被创建好了。
//2.若数组为空,说明数组还未创建,很有可能竞争的线程非常少,因此就直接 CAS 操作 baseCount
//若 CAS 成功,则方法跳转到 (2)处,若失败,则需要考虑给当前线程分配一个格子(指CounterCell对象)
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
//字面意思,是无竞争,这里先标记为 true,表示还没有产生线程竞争
boolean uncontended = true;
//这里有三种情况,会进入 fullAddCount 方法
//1.若数组为空,进方法 (1)
//2.ThreadLocalRandom.getProbe() 方法会给当前线程生成一个随机数(可以简单的认为也是一个hash值)
//然后用随机数与数组长度取模,计算它所在的格子。若当前线程所分配到的格子为空,进方法 (1)。
//3.若数组不为空,且线程所在格子不为空,则尝试 CAS 修改此格子对应的 value 值加1。
//若修改成功,则跳转到 (3),若失败,则把 uncontended 值设为 fasle,说明产生了竞争,然后进方法 (1)
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//方法(1), 这个方法的目的是让当前线程一定把 1 加成功。情况更多,更复杂,稍后讲。
fullAddCount(x, uncontended);
return;
}
//(3)能走到这,说明数组不为空,且修改 baseCount失败,
//且线程被分配到的格子不为空,且修改 value 成功。
//但是这里没明白为什么小于等于1,就直接返回了,这里我怀疑之前的方法漏掉了binCount=0的情况。
//而且此处若返回了,后边怎么判断扩容?(存疑)
if (check <= 1)
return;
//计算总共的元素个数
s = sumCount();
}
//(2)这里用于检查是否需要扩容(下边这部分很多逻辑不懂的话,等后边讲完扩容,再回来看就理解了)
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//若元素个数达到扩容阈值,且tab不为空,且tab数组长度小于最大容量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//这里假设数组长度n就为16,这个方法返回的是一个固定值,用于当做一个扩容的校验标识
//可以跳转到最后,看详细计算过程,0000 0000 0000 0000 1000 0000 0001 1011
int rs = resizeStamp(n);
//若sc小于0,说明正在扩容
if (sc < 0) {
//sc的结构类似这样,1000 0000 0001 1011 0000 0000 0000 0001
//sc的高16位是数据校验标识,低16位代表当前有几个线程正在帮助扩容,RESIZE_STAMP_SHIFT=16
//因此判断校验标识是否相等,不相等则退出循环
//sc == rs + 1,sc == rs + MAX_RESIZERS 这两个应该是用来判断扩容是否已经完成,但是计算方法存疑
//感兴趣的可以看这个地址,应该是一个 bug ,
// https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427
//nextTable=null 说明需要扩容的新数组还未创建完成
//transferIndex这个参数小于等于0,说明已经不需要其它线程帮助扩容了,
//但是并不说明已经扩容完成,因为有可能还有线程正在迁移元素。稍后扩容细讲就明白了。
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//到这里说明当前线程可以帮助扩容,因此sc值加一,代表扩容的线程数加1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//当sc大于0,说明sc代表扩容阈值,因此第一次扩容之前肯定走这个分支,用于初始化新表 nextTable
//rs<<16
//1000 0000 0001 1011 0000 0000 0000 0000
//+2
//1000 0000 0001 1011 0000 0000 0000 0010
//这个值,转为十进制就是 -2145714174,用于标识,这是扩容时,初始化新表的状态,
//扩容时,需要用到这个参数校验是否所有线程都全部帮助扩容完成。
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//扩容,第二个参数代表新表,传入null,则说明是第一次初始化新表(nextTable)
transfer(tab, null);
s = sumCount();
}
}
}
//扩容时的校验标识
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
//Integer.numberOfLeadingZeros方法的作用是返回 n 的最高位为1的前面的0的个数
//n=16,
0000 0000 0000 0000 0000 0000 0001 0000
//前面有27个0,即27
0000 0000 0000 0000 0000 0000 0001 1011
//RESIZE_STAMP_BITS为16,然后 1<<(16-1),即 1<<15
0000 0000 0000 0000 1000 0000 0000 0000
//它们做或运算,得到 rs 的值
0000 0000 0000 0000 1000 0000 0001 1011
sumCount 方法
计算桶(数组)的元素总个数,即将baseCount和CounterCell数组中的value值累加,它们之和就是桶的元素总个数。
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
//baseCount,以这个值作为累加基准
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
helpTransfer方法
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//头结点为 ForwardingNode ,并且新数组已经初始化
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) {
//若校验标识失败,或者已经扩容完成,或推进下标到头,则退出
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
//当前线程需要帮助迁移,sc值加1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
千金之法
fullAddCount 方法
addCount 方法中CounterCell数组不为空且CAS添加value失败后,进入到 fullAddCount 方法中。全力增加计数值,一定要成功。
首先判断数组CounterCell[]是否为空,则先去创建数组;如果数组正在被创建且还未空,则直接对 baseCount进行CAS增加,成功则方法结束,失败则继续自旋直到baseCount增加成功或数组创建完成;
如果数组不为空,可从以下逻辑图分析
//传过来的参数分别为 1 , false
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
//如果当前线程的随机数为0,则强制初始化一个值
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
//此时把 wasUncontended 设为true,认为无竞争
wasUncontended = true;
}
//用来表示比 contend(竞争)更严重的碰撞,若为true,表示可能需要扩容,以减少碰撞冲突
boolean collide = false; // True if last slot nonempty
//循环内,外层if判断分三种情况,内层判断又分为六种情况
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
//1. 若counterCells数组不为空。 建议先看下边的2和3两种情况,再回头看这个。
if ((as = counterCells) != null && (n = as.length) > 0) {
// (1) 若当前线程所在的格子(CounterCell对象)为空
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) {
//若无锁,则乐观的创建一个 CounterCell 对象。
CounterCell r = new CounterCell(x);
//尝试加锁
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
//加锁成功后,再 recheck 一下数组是否不为空,且当前格子为空
try {
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;
}
//若当前格子创建成功,且上边的赋值成功,则说明加1成功,退出循环
if (created)
break;
//否则,继续下次循环
continue; // Slot is now non-empty
}
}
//若cellsBusy=1,说明有其它线程抢锁成功。或者若抢锁的 CAS 操作失败,都会走到这里,
//则当前线程需跳转到(9)重新生成随机数,进行下次循环判断。
collide = false;
}
/**
*后边这几种情况,都是数组和当前随机到的格子都不为空的情况。
*且注意每种情况,若执行成功,且不break,continue,则都会执行(9),重新生成随机数,进入下次循环判断
*/
// (2) 到这,说明当前方法在被调用之前已经 CAS 失败过一次,若不明白可回头看下 addCount 方法,
//为了减少竞争,则跳转到⑨处重新生成随机数,并把 wasUncontended 设置为true ,认为下一次不会产生竞争
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// (3) 若 wasUncontended 为 true 无竞争,则尝试一次 CAS。若成功,则结束循环,若失败则判断后边的 (4)(5)(6)。
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
// (4) 结合 (6) 一起看,(4)(5)(6)都是 wasUncontended=true,且CAS修改value失败的情况。
//若数组有变化,或者数组长度大于等于当前CPU的核心数,则把 collide 改为 false
//因为数组若有变化,说明是由扩容引起的;长度超限,则说明已经无法扩容,只能认为无碰撞。
//这里很有意思,认真思考一下,当扩容超限后,则会达到一个平衡,即 (4)(5) 反复执行,直到 (3) 中CAS成功,跳出循环。
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
// (5) 若数组无变化,且数组长度小于CPU核心数时,且 collide 为 false,就把它改为 true,说明下次循环可能需要扩容
else if (!collide)
collide = true;
// (6) 若数组无变化,且数组长度小于CPU核心数时,且 collide 为 true,说明冲突比较严重,需要扩容了。
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
//recheck
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;
}
//认为扩容后,下次不会产生冲突了,和(4)处逻辑照应
collide = false;
//当次扩容后,就不需要重新生成随机数了
continue; // Retry with expanded table
}
// (9),重新生成一个随机数,进行下一次循环判断
h = ThreadLocalRandom.advanceProbe(h);
}
//2.这里的 cellsBusy 参数非常有意思,是一个volatile的 int值,用来表示自旋锁的标志,
//可以类比 AQS 中的 state 参数,用来控制锁之间的竞争,并且是独占模式。简化版的AQS。
//cellsBusy 若为0,说明无锁,线程都可以抢锁,若为1,表示已经有线程拿到了锁,则其它线程不能抢锁。
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try {
//这里再重新检测下 counterCells 数组引用是否有变化
if (counterCells == as) {
//初始化一个长度为 2 的数组
CounterCell[] rs = new CounterCell[2];
//根据当前线程的随机数值,计算下标,只有两个结果 0 或 1,并初始化对象
rs[h & 1] = new CounterCell(x);
//更新数组引用
counterCells = rs;
//初始化成功的标志
init = true;
}
} finally {
//别忘了,需要手动解锁。
cellsBusy = 0;
}
//若初始化成功,则说明当前加1的操作也已经完成了,则退出整个循环。
if (init)
break;
}
//3.到这,说明数组为空,且 2 抢锁失败,则尝试直接去修改 baseCount 的值,
//若成功,也说明加1操作成功,则退出循环。
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
transfer 方法
//这个类是一个标志,用来代表当前桶(数组中的某个下标位置)的元素已经全部迁移完成
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
//把当前桶的头结点的 hash 值设置为 -1,表明已经迁移完成,
//这个节点中并不存储有效的数据
super(MOVED, null, null, null);
this.nextTable = tab;
}
}
//迁移数据
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//根据当前CPU核心数,确定每次推进的步长,最小值为16.(为了方便我们以2为例)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//从 addCount 方法,只会有一个线程跳转到这里,初始化新数组
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
//新数组长度为原数组的两倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
//用 nextTable 指代新数组
nextTable = nextTab;
//这里就把推进的下标值初始化为原数组长度(以16为例)
transferIndex = n;
}
//新数组长度
int nextn = nextTab.length;
//创建一个标志类
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//是否向前推进的标志
boolean advance = true;
//是否所有线程都全部迁移完成的标志
boolean finishing = false; // to ensure sweep before committing nextTab
//i 代表当前线程正在迁移的桶的下标,bound代表它本次可以迁移的范围下限
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//需要向前推进
while (advance) {
int nextIndex, nextBound;
//(1) 先看 (3) 。i每次自减 1,直到 bound。若超过bound范围,或者finishing标志为true,则不用向前推进。
//若未全部完成迁移,且 i 并未走到 bound,则跳转到 (7),处理当前桶的元素迁移。
if (--i >= bound || finishing)
advance = false;
//(2) 每次执行,都会把 transferIndex 最新的值同步给 nextIndex
//若 transferIndex小于等于0,则说明原数组中的每个桶位置,都有线程在处理迁移了,
//于是,需要跳出while循环,并把 i设为 -1,以跳转到④判断在处理的线程是否已经全部完成。
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//(3) 第一个线程会先走到这里,确定它的数据迁移范围。(2)处会更新 nextIndex为 transferIndex 的最新值
//因此第一次 nextIndex=n=16,nextBound代表当次迁移的数据范围下限,减去步长即可,
//所以,第一次时,nextIndex=16,nextBound=16-2=14。后续,每次都会间隔一个步长。
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//bound代表当次数据迁移下限
bound = nextBound;
//第一次的i为15,因为长度16的数组,最后一个元素的下标为15
i = nextIndex - 1;
//表明不需要向前推进,只有当把当前范围内的数据全部迁移完成后,才可以向前推进
advance = false;
}
}
//(4)
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//若全部线程迁移完成
if (finishing) {
nextTable = null;
//更新table为新表
table = nextTab;
//扩容阈值改为原来数组长度的 3/2 ,即新长度的 3/4,也就是新数组长度的0.75倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//到这,说明当前线程已经完成了自己的所有迁移(无论参与了几次迁移),
//则把 sc 减1,表明参与扩容的线程数减少 1。
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//在 addCount 方法最后,我们强调,迁移开始时,会设置 sc=(rs << RESIZE_STAMP_SHIFT) + 2
//每当有一个线程参与迁移,sc 就会加 1,每当有一个线程完成迁移,sc 就会减 1。
//因此,这里就是去校验当前 sc 是否和初始值是否相等。相等,则说明全部线程迁移完成。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//只有此处,才会把finishing 设置为true。
finishing = advance = true;
//这里非常有意思,会把 i 从 -1 修改为16,
//目的就是,让 i 再从后向前扫描一遍数组,检查是否所有的桶都已被迁移完成,参看 (6)
i = n; // recheck before commit
}
}
//(5) 若i的位置元素为空,则说明当前桶的元素已经被迁移完成,就把头结点设置为fwd标志。
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//(6) 若当前桶的头结点是 ForwardingNode ,说明迁移完成,则向前推进
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
//(7) 处理当前桶的数据迁移。
else {
synchronized (f) { //给头结点加锁
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
//若hash值大于等于0,则说明是普通链表节点
if (fh >= 0) {
int runBit = fh & n;
//这里是 1.7 的 CHM 的 rehash 方法和 1.8 HashMap的 resize 方法的结合体。
//会分成两条链表,一条链表和原来的下标相同,另一条链表是原来的下标加数组长度的位置
//然后找到 lastRun 节点,从它到尾结点整体迁移。
//lastRun前边的节点则单个迁移,但是需要注意的是,这里是头插法。
//另外还有一点和1.7不同,1.7 lastRun前边的节点是复制过去的,而这里是直接迁移的,没有复制操作。
//所以,最后会有两条链表,一条链表从 lastRun到尾结点是正序的,而lastRun之前的元素是倒序的,
//另外一条链表,从头结点开始就是倒叙的。看下图。
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
//树节点
else if (f instanceof TreeBin) {
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 底层原理了
JDK1.8并发容器ConcurrentHashMap - 掘金 (juejin.cn)
《吊打面试官》系列-ConcurrentHashMap & Hashtable
面试必备之ConcurrentHashMap终结篇_哔哩哔哩_bilibili