目录
2.3、putVal方法 - 添加数据到数组 & 初始化数组
3.2、tryPreSize方法-针对putAll的初始化操作
为什么用 MAXIMUM_CAPACITY >>> 1 而不是 MAXIMUM_CAPACITY?
3.4、tryPreSize方法-对sizeCtl的修改以及条件判断的BUG
一、存储结构:数组+链表+红黑树
1、concurrentHashMap 是线程安全的HashMap。
早期线程安全的是HashTabel,但是这个现在不用了。
Hashtable
的设计缺陷
粗粒度锁:
Hashtable
使用实例级别的锁(即锁住整个对象),所有操作需要竞争同一把锁。即使多个线程操作不同的键值对,也会被阻塞。性能问题:高并发场景下,锁竞争激烈,吞吐量显著下降。
2、concurrentHashMap 在JDK1.8中是以 CAS+synchronized实现的线程安全
- CAS:在没有hash冲突时(Node要放在数组上)
- synchronized:在出现hash冲突时(Node存放的位置已经有数据了)
3、存储结构:数组+链表+红黑树
二、存储操作
2.1、put方法
public V put(K key, V value) {
// 在调用put方法时,会调用putVal方法,第三个参数传递false
// 在调用putIfAbsent是,会调用putVal方法,第三个参数传递true
// fasle: key一致时,直接覆盖value
// true: key一致时,什么都不做,key不存在时,正常添加。
return putVal(key, value, false);
}
2.2、putVal方法 - 散列算法
这里主要是看spread方法
final V putVal(K key, V value, boolean onlyIfAbsent) { // ConcurrentHashMap不允许key或者value出现为null的值,跟HashMap的区别 if (key == null || value == null) throw new NullPointerException(); // 根据key的hashCode计算出一个hash值,后期得出当前key-value要存储在哪个数组索引位置 int hash = spread(key.hashCode()); // 一个标识,在后面有用! int binCount = 0; // 省略大量的代码…… }
// 计算当前Node的hash值的方法 static final int spread(int h) { // 将key的hashCode值的高低16位进行^运算,最终又与HASH_BITS进行了&运算 // 将高位的hash也参与到计算索引位置的运算当中 // 为什么HashMap、ConcurrentHashMap,都要求数组长度为2^n // HASH_BITS让hash值的最高位符号位肯定为0,代表当前hash值默认情况下一定是正数,因为hash值为负数时,有特殊的含义 // static final int MOVED = -1; // 代表当前hash位置的数据正在扩容! // static final int TREEBIN = -2; // 代表当前hash位置下挂载的是一个红黑树 // static final int RESERVED = -3; // 预留当前索引位置…… return (h ^ (h >>> 16)) & HASH_BITS; // 计算数组放到哪个索引位置的方法 (f = tabAt(tab, i = (n - 1) & hash) // n:是数组的长度 } 00001101 00001101 00101111 10001111 - h = key.hashCode 运算方式 00000000 00000000 00000000 00001111 - 15 (n - 1) & ( ( 00001101 00001101 00101111 10001111 - h ^ 00000000 00000000 00001101 00001101 - h >>> 16 ) & 01111111 11111111 11111111 11111111 - HASH_BITS )
2.3、putVal方法 - 添加数据到数组 & 初始化数组
1
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; // ===========上面的说过了=========== // 死循环, 将当前ConcurrentHashMap中的数组赋值给变量tab for (Node<K,V>[] tab = table;;) { // 声明变量 f 、n、 i、 fh Node<K,V> f; int n, i, fh; // 当数组不存在时(第一次插入数据),对数组做初始化 // 从这里可以看出putVal 其实是"懒加载"。 // n:数组长度 if (tab == null || (n = tab.length) == 0) tab = initTable(); // i:(n - 1) & hash : 计算出当前Node应该存放在哪个索引位置 // tabAt方法 :获取i位置上的数据。 // f:当前数组i索引位置上的Node对象 // 当前if判断逻辑:若当前索引位置没有数据 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // casTabAt:基于CAS的方式将数据放在i索引位置的数组上 if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) // 如果成功,break跳出循环,插入数据成功 break; } // 前提:数组存在,但是当前索引位置有数据。 // 当前if判断逻辑:判断当前位置数据是否正在扩容。(MOVED = -1) // fn:当前数组i索引位置上数据的hash值 else if ((fh = f.hash) == MOVED) // helpTransfer :让当前插入数据的线程协助扩容 tab = helpTransfer(tab, f); //===========下面的代码是添加到链表的操作 后面再讲 先把initTable()方法看了=========== else { V oldVal = null; synchronized (f) { if (tabAt(tab, i) == f) { 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) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }
initTable 方法
- sizeCtl:是数组在初始化和扩容操作时的一个控制变量
- -1: 代表当前数组正在初始化
- <-1:低16位代表当前数组正在扩容的线程个数(如果1个线程扩容,值为-2,如果2个线程扩容,值为-3)
- 0: 代表数组还没初始化
- >0: 代表当前数组的扩容阈值,或者是当前数组的初始化大小
/* sizeCtl:是数组在初始化和扩容操作时的一个控制变量 -1: 代表当前数组正在初始化 <-1:低16位代表当前数组正在扩容的线程个数(如果1个线程扩容,值为-2,如果2个线程扩容,值为-3) 0: 代表数组还没初始化 >0: 代表当前数组的扩容阈值,或者是当前数组的初始化大小 */ private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; // while 循环判断 当前数组是否为空 while ((tab = table) == null || tab.length == 0) { // sizeCtl:是数组在初始化和扩容操作时的一个控制变量 if ((sc = sizeCtl) < 0) /* 当线程检测到其他线程正在初始化(sizeCtl < 0),会进入自旋等待。 调用yield()目的 : 1、减少自旋等待的开销。 2、提高初始化线程的优先级:帮助初始化线程更快的完成初始化 3、避免线饥饿:可以平衡各个线程的执行机会 */ Thread.yield(); // 尝试初始化数组。线程会以CAS的方式,将sizeCtl修改为-1,代表当前线程可以初始化数组 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 初始化.... try { // 再次判断当前数组是否已经初始化完毕。 if ((tab = table) == null || tab.length == 0) { // 开始初始化, // 如果sizeCtl > 0,就初始化sizeCtl长度的数组 // 如果sizeCtl == 0,就初始化默认的长度(16) int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 初始化数组! Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 将初始化的数组nt,赋值给tab和table table = tab = nt; // sc赋值为了数组长度 - 数组长度 右移 2位 16 - 4 = 12 // 将sc赋值为下次扩容的阈值 sc = n - (n >>> 2); } } finally { // 将赋值好的sc,设置给sizeCtl sizeCtl = sc; } break; } } return tab; }
2.4、putVal方法 - 添加数据到链表
1
// binCount: 在链表情况下,记录链表长度的一个标识 // n:数组长度 // i:当前Node需要存放的索引位置 // f: 当前数组i索引位置的Node对象 // fn:当前数组i索引位置上数据的hash值 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) tab = helpTransfer(tab, f); //===========下面的代码是添加到链表/红黑树的操作 =========== // 声明变量为oldVal V oldVal = null; // 基于当前索引位置的Node,作为锁对象…… synchronized (f) { // 判断当前位置的数据还是之前的f么……(避免并发操作的安全问题) if (tabAt(tab, i) == f) { // 再次判断hash值是否大于0(不是树) if (fh >= 0) { // binCount设置为1(在链表情况下,记录链表长度的一个标识) binCount = 1; // 死循环,每循环一次,对binCount for (Node<K,V> e = f;; ++binCount) { // 声明标识ek K ek; // 当前i索引位置的数据,是否和当前put的key的hash值一致 if (e.hash == hash && // 如果当前i索引位置数据的key和put的key == 返回为true // 或者equals相等 ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // key一致,可能需要覆盖数据! // 当前i索引位置数据的value复制给oldVal oldVal = e.val; // 如果传入的是false,代表key一致,覆盖value // 如果传入的是true,代表key一致,什么都不做! if (!onlyIfAbsent) // 覆盖value e.val = value; break; } // 拿到当前指定的Node对象 Node<K,V> pred = e; // 将e指向下一个Node对象,如果next指向的是一个null,可以挂在当前Node下面 if ((e = e.next) == null) { // 将hash,key,value封装为Node对象,挂在pred的next上 pred.next = new Node<K,V>(hash, key, value, null); break; } } } // ======== 这部分代码先不看 后面会讲到 STRAT =========== 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; } } // ======== END =========== } } if (binCount != 0) { // 判断链表长度是否 >= 8 if (binCount >= TREEIFY_THRESHOLD) /* 1、尝试转为红黑树或者扩容 2、基于treeifyBin方法和上面的if判断,可以得知链表想要转为红黑树,必须保证数组长度大于等于64,并且链表长度大于等于8 3、如果数组长度没有达到64的话,会首先将数组扩容,因为在数组上操作数据是效率最高的 */ treeifyBin(tab, i); // 这里oldVal不为空,证明发生了key的value覆盖情况 if (oldVal != null) // 返回之前的数据 return oldVal; break; } } } addCount(1L, binCount); return null; }
扩容要素
- 链表想要转为红黑树,必须保证数组长度大于等于64,并且链表长度大于等于8
- 如果数组长度没有达到64的话,会首先将数组扩容,因为在数组上操作数据是效率最高的
- 扩容阈值:当前数组长度 - 数组长度 右移 2位(3/4)
- 为什么链表长度为8转换为红黑树,不是能其他数值嘛?
因为布松分布
The main disadvantage of per-bin locks is that other update
* operations on other nodes in a bin list protected by the same
* lock can stall, for example when user equals() or mapping
* functions take a long time. However, statistically, under
* random hash codes, this is not a common problem. Ideally, the
* frequency of nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average, given the resizing threshold
* of 0.75, although with a large variance because of resizing
* granularity. Ignoring variance, the expected occurrences of
* list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The
* first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
三、扩容操作
3.1、treeifyBin方法触发扩容
// 在链表长度大于等于8时,尝试将链表转为红黑树 private final void treeifyBin(Node<K,V>[] tab, int index) { Node<K,V> b; int n, sc; // 数组不能为空 if (tab != null) { // 数组的长度n,是否小于64 if ((n = tab.length) < MIN_TREEIFY_CAPACITY) // 如果数组长度小于64,不能将链表转为红黑树,先尝试扩容操作 // 这个方法具体实现在下面会讲到 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)); } } } } }
3.2、tryPreSize方法-针对putAll的初始化操作
// size是将之前的数组长度 左移 1位得到的结果 private final void tryPresize(int size) { // 如果扩容的长度达到了最大值,就使用最大值 // 否则需要保证数组的长度为2的n次幂 -- tableSizeFor()通过这个方法保证 // 这块的操作,是为了初始化操作准备的,因为调用putAll方法时,也会触发tryPresize方法 // 如果刚刚new的ConcurrentHashMap直接调用了putAll方法的话,会通过tryPresize方法进行初始化 int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1); // 这些代码和initTable一模一样 // 声明sc int sc; // 将sizeCtl的值赋值给sc,并判断是否大于0,这里代表没有初始化操作,也没有扩容操作 while ((sc = sizeCtl) >= 0) { // 将ConcurrentHashMap的table赋值给tab,并声明数组长度n Node<K,V>[] tab = table; int n; // 数组是否需要初始化 if (tab == null || (n = tab.length) == 0) { // 进来执行初始化 // sc是初始化长度,初始化长度如果比计算出来的c要大的话,直接使用sc,如果没有sc大, // 说明sc无法容纳下putAll中传入的map,使用更大的数组长度 n = (sc > c) ? sc : c; // 设置sizeCtl为-1,代表初始化操作 if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // 再次判断数组的引用有没有变化 if (table == tab) { // 初始化数组 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 数组赋值 table = nt; // 计算扩容阈值 sc = n - (n >>> 2); } } finally { // 最终赋值给sizeCtl sizeCtl = sc; } } } // 如果计算出来的长度c如果小于等于sc,就不需要扩容了,直接退出循环结束方法 // 数组长度大于等于最大长度了,直接退出循环结束方法 else if (c <= sc || n >= MAXIMUM_CAPACITY) break; // 省略部分代码 } } // 将c这个长度设置到最近的2的n次幂的值, // c == size + (size >>> 1) + 1 // size = 17 00000000 00000000 00000000 00010001 + 00000000 00000000 00000000 00001000 + 00000000 00000000 00000000 00000001 // c = 26 00000000 00000000 00000000 00011010 // 返回大于等于c的最小幂次方 private static final int tableSizeFor(int c) { // 00000000 00000000 00000000 00011001 int n = c - 1; // 00000000 00000000 00000000 00011001 // 00000000 00000000 00000000 00001100 // 00000000 00000000 00000000 00011101 n |= n >>> 1; // 00000000 00000000 00000000 00011101 // 00000000 00000000 00000000 00000111 // 00000000 00000000 00000000 00011111 n |= n >>> 2; // 00000000 00000000 00000000 00011111 // 00000000 00000000 00000000 00000001 // 00000000 00000000 00000000 00011111 n |= n >>> 4; // 00000000 00000000 00000000 00011111 // 00000000 00000000 00000000 00000000 // 00000000 00000000 00000000 00011111 n |= n >>> 8; // 00000000 00000000 00000000 00011111 n |= n >>> 16; // 00000000 00000000 00000000 00100000 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);为什么用
MAXIMUM_CAPACITY >>> 1
而不是MAXIMUM_CAPACITY
?原因 1:防止无效的容量计算
如果直接判断
size >= MAXIMUM_CAPACITY
,则只有当size
已经达到最大容量时才会触发设置MAXIMUM_CAPACITY
。但
size
可能尚未达到最大容量,但按翻倍策略计算出的目标容量会超过MAXIMUM_CAPACITY
,此时必须强制限制为最大容量。原因 2:优化扩容效率
当
size
接近最大容量的一半时,翻倍扩容后的容量可能已经接近或超过MAXIMUM_CAPACITY
。
例如:
MAXIMUM_CAPACITY = 1 << 30
,则MAXIMUM_CAPACITY >>> 1 = 1 << 29
。若
size = 1 << 29
,则size + (size >>> 1) + 1 = 1 << 29 + 1 << 28 + 1 ≈ 1.5 << 29
。
tableSizeFor(1.5 << 29)
会返回1 << 30
(即MAXIMUM_CAPACITY
)。直接设置阈值,避免重复调用
tableSizeFor
计算,提升效率。原因 3:避免溢出风险
若
size
已经接近MAXIMUM_CAPACITY
,size + (size >>> 1) + 1
可能导致整数溢出(超出int
范围)。提前判断
size >= (MAXIMUM_CAPACITY >>> 1)
,可以规避潜在溢出问题
3.3、tryPreSize方法-计算扩容戳并且查看BUG
private final void tryPresize(int size) { // n:数组长度 while ((sc = sizeCtl) >= 0) { // 判断当前的tab是否和table一致,若不一致说明其他线程已经完成扩容,当前线程则不需要处理 else if (tab == table) { // 计算扩容标识戳,根据当前数组的长度计算一个16位的扩容戳 // 第一个作用是为了保证后面的sizeCtl赋值时,保证sizeCtl为小于-1的负数 // 第二个作用用来记录当前是从什么长度开始扩容的 int rs = resizeStamp(n); // 如果sc小于0,代表有线程正在扩容。 if (sc < 0) { // 省略部分代码……协助扩容的代码 } // 代表没有线程正在扩容,当前线程是第一个扩容的。 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) // 省略部分代码……第一个扩容的线程…… } } } // 计算扩容表示戳 // 32 = 00000000 00000000 00000000 00100000 // Integer.numberOfLeadingZeros(32) = 26 // 1 << (RESIZE_STAMP_BITS - 1) // 00000000 00000000 10000000 00000000 // 00000000 00000000 00000000 00011010 // 00000000 00000000 10000000 00011010 static final int resizeStamp(int n) { return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)); }
3.4、tryPreSize方法-对sizeCtl的修改以及条件判断的BUG
private final void tryPresize(int size) { // sc默认为sizeCtl while ((sc = sizeCtl) >= 0) { else if (tab == table) { // rs:扩容戳 00000000 00000000 10000000 00011010 int rs = resizeStamp(n); if (sc < 0) { // 说明有线程正在扩容,过来帮助扩容 Node<K,V>[] nt; // 依然有BUG // 当前线程扩容时,老数组长度是否和我当前线程扩容时的老数组长度一致 // 00000000 00000000 10000000 00011010 if ((sc >>> RESIZE_STAMP_SHIFT) != rs // 10000000 00011010 00000000 00000010 // 00000000 00000000 10000000 00011010 // 这两个判断都是有问题的,核心问题就应该先将rs左移16位,再追加当前值。 // 这两个判断是BUG // 判断当前扩容是否已经即将结束 || sc == rs + 1 // sc == rs << 16 + 1 BUG // 判断当前扩容的线程是否达到了最大限度 || sc == rs + MAX_RESIZERS // sc == rs << 16 + MAX_RESIZERS BUG // 扩容已经结束了。 || (nt = nextTable) == null // 记录迁移的索引位置,从高位往低位迁移,也代表扩容即将结束。 || transferIndex <= 0) break; // 如果线程需要协助扩容,首先就是对sizeCtl进行+1操作,代表当前要进来一个线程协助扩容 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) // 上面的判断没进去的话,nt就代表新数组 transfer(tab, nt); } // 是第一个来扩容的线程 // 基于CAS将sizeCtl修改为 10000000 00011010 00000000 00000010 // 将扩容戳左移16位之后,符号位是1,就代码这个值为负数 // 低16位在表示当前正在扩容的线程有多少个, // 为什么低位值为2时,代表有一个线程正在扩容 // 每一个线程扩容完毕后,会对低16位进行-1操作,当最后一个线程扩容完毕后,减1的结果还是-1, // 当值为-1时,要对老数组进行一波扫描,查看是否有遗漏的数据没有迁移到新数组 else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)) // 调用transfer方法,并且将第二个参数设置为null,就代表是第一次来扩容! transfer(tab, null); } } }
关键设计点
扩容标记与线程数分离
sizeCtl
的高 16 位存储扩容标记,低 16 位存储扩容线程数 + 1。例如:
sizeCtl = (rs << 16) + 3
表示有 2 个线程正在扩容。多线程协作机制
已扩容的线程通过
transferIndex
分配迁移区间,每个线程处理一段连续桶。新线程通过 CAS 增加
sizeCtl
的线程数,加入迁移任务。状态校验
通过检查
nextTable
和transferIndex
,确保扩容环境有效。避免重复参与或操作无效的扩容任务。
3.5、transfer方法-方法签名与变量初始化
// 开始扩容 tab=oldTable private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length; // 旧表长度 int stride; // 每个线程处理的桶区间跨度(一次性迁移多少数据到新数组) // 基于CPU的内核数量来计算,每个线程一次性迁移多少长度的数据最合理 // NCPU = 4 // 举个栗子:数组长度为1024 - 512 - 256 - 128 / 4 = 32 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // 最小跨度为 16 // 初始化新表 nextTab(仅在首次调用时执行) if (nextTab == null) { try { // 容量翻倍:n << 1 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; nextTab = nt; } catch (Throwable ex) { sizeCtl = Integer.MAX_VALUE; // 扩容失败,回退 return; } nextTable = nextTab; // 更新全局新表引用 transferIndex = n; // 迁移起点为旧表末尾(从后向前迁移) } int nextn = nextTab.length; ForwardingNode<K,V> fwd = new ForwardingNode<>(nextTab); // 占位节点,标记已迁移的桶 boolean advance = true; // 控制迁移进度的标志 boolean finishing = false; // 标记是否完成所有迁移 // ... 后续逻辑 }
关键点:
stride
计算:根据 CPU 核心数分配每个线程处理的桶区间,避免任务分配不均。新表初始化:仅在首次扩容时创建容量翻倍的新表。
ForwardingNode
:用于标记已迁移的桶,其他线程遇到时会跳过或协助迁移。
3.6、transfer方法-外层循环:分配迁移区间
for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; // 分配当前线程处理的区间 [bound, i) while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) { // 当前区间未处理完 advance = false; } else if ((nextIndex = transferIndex) <= 0) { // 所有桶已分配完毕 i = -1; advance = false; } else if (U.compareAndSwapInt( this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { // CAS 更新 transferIndex,分配区间 [nextBound, nextIndex) bound = nextBound; // 当前线程负责的下界 i = nextIndex - 1; // 当前线程处理的起始桶索引 advance = false; } } // ... 处理单个桶的迁移 }
3.7、transfer方法-迁移结束操作
// 以32长度扩容到64位为例子 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { for (int i = 0, bound = 0;;) { while (advance) { // 判断扩容是否已经结束! // i < 0:当前线程没有接收到任务! // i >= n: 迁移的索引位置,不可能大于数组的长度,不会成立 // i + n >= nextn:因为i最大值就是数组索引的最大值,不会成立 if (i < 0 || i >= n || i + n >= nextn) { // 如果进来,代表当前线程没有接收到任务 int sc; // finishing为true,代表扩容结束 if (finishing) { // 将nextTable新数组设置为null nextTable = null; // 将当前数组的引用指向了新数组~ table = nextTab; // 重新计算扩容阈值 64 - 16 = 48 sizeCtl = (n << 1) - (n >>> 1); // 结束扩容 return; } // 当前线程没有接收到任务,让当前线程结束扩容操作。 // 采用CAS的方式,将sizeCtl - 1,代表当前并发扩容的线程数 - 1 if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // sizeCtl的高16位是基于数组长度计算的扩容戳,低16位是当前正在扩容的线程个数 if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) // 代表当前线程并不是最后一个退出扩容的线程,直接结束当前线程扩容 return; // 如果是最后一个退出扩容的线程,将finishing和advance设置为true finishing = advance = true; // 将i设置为老数组长度,让最后一个线程再从尾到头再次检查一下,是否数据全部迁移完毕。 i = n; } } // 开始迁移数据,并且在迁移完毕后,会将advance设置为true } }
3.8、transfer方法-迁移数据(链表)
// 以32长度扩容到64位为例子 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { // 省略部分代码………… for (int i = 0, bound = 0;;) { // 省略部分代码………… if (i < 0 || i >= n || i + n >= nextn) { // 省略部分代码………… } // 开始迁移数据,并且在迁移完毕后,会将advance设置为true // 获取指定i位置的Node对象,并且判断是否为null else if ((f = tabAt(tab, i)) == null) // 当前桶位置没有数据,无需迁移,直接将当前桶位置设置为fwd advance = casTabAt(tab, i, null, fwd); // 拿到当前i位置的hash值,如果为MOVED,证明数据已经迁移过了。 else if ((fh = f.hash) == MOVED) // 一般是给最后扫描时,使用的判断,如果迁移完毕,直接跳过当前位置。 advance = true; // already processed else { // 当前桶位置有数据,先锁住当前桶位置。 synchronized (f) { // 判断之前取出的数据是否为当前的数据。 if (tabAt(tab, i) == f) { // ln:null - lowNode // hn:null - highNode Node<K,V> ln, hn; // hash大于0,代表当前Node属于正常情况,不是红黑树,使用链表方式迁移数据 if (fh >= 0) { // lastRun机制 // 000000000010000 // 这种运算结果只有两种,要么是0,要么是n int runBit = fh & n; // 将f赋值给lastRun Node<K,V> lastRun = f; // 循环的目的就是为了得到链表下经过hash & n结算,结果一致的最后一些数据 // 在迁移数据时,值需要迁移到lastRun即可,剩下的指针不需要变换。 for (Node<K,V> p = f.next; p != null; p = p.next) { int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } // runBit == 0,赋值给ln if (runBit == 0) { ln = lastRun; hn = null; } // rubBit == n,赋值给hn else { hn = lastRun; ln = null; } // 循环到lastRun指向的数据即可,后续不需要再遍历 for (Node<K,V> p = f; p != lastRun; p = p.next) { // 获取当前Node的hash值,key值,value值。 int ph = p.hash; K pk = p.key; V pv = p.val; // 如果hash&n为0,挂到lowNode上 if ((ph & n) == 0) ln = new Node<K,V>(ph, pk, pv, ln); // 如果hash&n为n,挂到highNode上 else hn = new Node<K,V>(ph, pk, pv, hn); } // 采用CAS的方式,将ln挂到新数组的原位置 setTabAt(nextTab, i, ln); // 采用CAS的方式,将hn挂到新数组的原位置 + 老数组长度 setTabAt(nextTab, i + n, hn); // 采用CAS的方式,将当前桶位置设置为fwd setTabAt(tab, i, fwd); // advance设置为true,保证可以进入到while循环,对i进行--操作 advance = true; } // 省略迁移红黑树的操作 } } } } }
3.9、helpTransfer方法-协助扩容
// 在添加数据时,如果插入节点的位置的数据,hash值为-1,代表当前索引位置数据已经被迁移到了新数组 // tab:老数组 // f:数组上的Node节点 final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { // nextTab:新数组 // sc:给sizeCtl做临时变量 Node<K,V>[] nextTab; int sc; // 第一个判断:老数组不为null // 第二个判断:新数组不为null (将新数组赋值给nextTab) if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { // ConcurrentHashMap正在扩容 // 基于老数组长度计算扩容戳 int rs = resizeStamp(tab.length); // 第一个判断:fwd中的新数组,和当前正在扩容的新数组是否相等。 相等:可以协助扩容。不相等:要么扩容结束,要么开启了新的扩容 // 第二个判断:老数组是否改变了。 相等:可以协助扩容。不相等:扩容结束了 // 第三个判断:如果正在扩容,sizeCtl肯定为负数,并且给sc赋值 while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { // 第一个判断:将sc右移16位,判断是否与扩容戳一致。 如果不一致,说明扩容长度不一样,退出协助扩容 // 第二个、三个判断是BUG: /* sc == rs << 16 + 1 || 如果+1和当前sc一致,说明扩容已经到了最后检查的阶段 sc == rs << 16 + MAX_RESIZERS || 判断协助扩容的线程是否已经达到了最大值 */ // 第四个判断:transferIndex是从高索引位置到低索引位置领取数据的一个核心属性,如果满足 小于等于0,说明任务被领光了。 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) // 不需要协助扩容 break; // 将sizeCtl + 1,进来协助扩容 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { // 协助扩容 transfer(tab, nextTab); break; } } return nextTab; } return table; }
四、红黑树操作
https://blog.youkuaiyun.com/weixin_49622776/article/details/146298428?spm=1001.2014.3001.5502