ConcurrentHashMap源码分析(JDK8) 扩容实现机制

jdk8中,采用多线程扩容。整个扩容过程,通过CAS设置sizeCtl,transferIndex等变量协调多个线程进行并发扩容

扩容相关的属性

nextTable

扩容期间,将table数组中的元素 迁移到 nextTable。

 
  1.  
  2. /**

  3. * The next table to use; non-null only while resizing.

  4. 扩容时,将table中的元素迁移至nextTable . 扩容时非空

  5. */

  6. private transient volatile Node<K,V>[] nextTable;

  7.  
  8.  

sizeCtl属性

 
  1.  
  2. private transient volatile int sizeCtl;

  3.  

多线程之间,以volatile的方式读取sizeCtl属性,来判断ConcurrentHashMap当前所处的状态。通过cas设置sizeCtl属性,告知其他线程ConcurrentHashMap的状态变更

不同状态,sizeCtl所代表的含义也有所不同。

  • 未初始化:
    • sizeCtl=0:表示没有指定初始容量。
    • sizeCtl>0:表示初始容量。
  • 初始化中:

    • sizeCtl=-1,标记作用,告知其他线程,正在初始化
  • 正常状态:

    • sizeCtl=0.75n ,扩容阈值
  • 扩容中:

    • sizeCtl < 0 : 表示有其他线程正在执行扩容
    • sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示此时只有一个线程在执行扩容

ConcurrentHashMap的状态图如下:

image.png

transferIndex属性

 
  1. private transient volatile int transferIndex;

  2.  
  3.  
  4. /**

  5. 扩容线程每次最少要迁移16个hash桶

  6. */

  7. private static final int MIN_TRANSFER_STRIDE = 16;

扩容索引,表示已经分配给扩容线程的table数组索引位置。主要用来协调多个线程,并发安全地
获取迁移任务(hash桶)。

1 在扩容之前,transferIndex 在数组的最右边 。此时有一个线程发现已经到达扩容阈值,准备开始扩容。

image.png

2 扩容线程,在迁移数据之前,首先要将transferIndex右移(以cas的方式修改 transferIndex=transferIndex-stride(要迁移hash桶的个数)),获取迁移任务。每个扩容线程都会通过for循环+CAS的方式设置transferIndex,因此可以确保多线程扩容的并发安全。

image.png

换个角度,我们可以将待迁移的table数组,看成一个任务队列,transferIndex看成任务队列的头指针。而扩容线程,就是这个队列的消费者。扩容线程通过CAS设置transferIndex索引的过程,就是消费者从任务队列中获取任务的过程。为了性能考虑,我们当然不会每次只获取一个任务(hash桶),因此ConcurrentHashMap规定,每次至少要获取16个迁移任务(迁移16个hash桶,MIN_TRANSFER_STRIDE = 16)

cas设置transferIndex的源码如下:

 
  1. private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {

  2. //计算每次迁移的node个数

  3. if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)

  4. stride = MIN_TRANSFER_STRIDE; // 确保每次迁移的node个数不少于16个

  5. ...

  6. for (int i = 0, bound = 0;;) {

  7. ...

  8. //cas无锁算法设置 transferIndex = transferIndex - stride

  9. if (U.compareAndSwapInt

  10. (this, TRANSFERINDEX, nextIndex,

  11. nextBound = (nextIndex > stride ?

  12. nextIndex - stride : 0))) {

  13. ...

  14. ...

  15. }

  16. ...//省略迁移逻辑

  17. }

  18. }

  19.  

ForwardingNode节点

  1. 标记作用,表示其他线程正在扩容,并且此节点已经扩容完毕

  2. 关联了nextTable,扩容期间可以通过find方法,访问已经迁移到了nextTable中的数据

 
  1. static final class ForwardingNode<K,V> extends Node<K,V> {

  2. final Node<K,V>[] nextTable;

  3. ForwardingNode(Node<K,V>[] tab) {

  4. //hash值为MOVED(-1)的节点就是ForwardingNode

  5. super(MOVED, null, null, null);

  6. this.nextTable = tab;

  7. }

  8. //通过此方法,访问被迁移到nextTable中的数据

  9. Node<K,V> find(int h, Object k) {

  10. ...

  11. }

  12. }

何时扩容

1 当前容量超过阈值

 
  1. final V putVal(K key, V value, boolean onlyIfAbsent) {

  2. ...

  3. addCount(1L, binCount);

  4. ...

  5. }

 
  1. private final void addCount(long x, int check) {

  2. ...

  3. if (check >= 0) {

  4. Node<K,V>[] tab, nt; int n, sc;

  5. //s>=sizeCtl 即容量达到扩容阈值,需要扩容

  6. while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&

  7. (n = tab.length) < MAXIMUM_CAPACITY) {

  8. //调用transfer()扩容

  9. ...

  10. }

  11. }

  12. }

2 当链表中元素个数超过默认设定(8个),当数组的大小还未超过64的时候,此时进行数组的扩容,如果超过则将链表转化成红黑树

 
  1. final V putVal(K key, V value, boolean onlyIfAbsent) {

  2. ...

  3. if (binCount != 0) {

  4. //链表中元素个数超过默认设定(8个)

  5. if (binCount >= TREEIFY_THRESHOLD)

  6. treeifyBin(tab, i);

  7. if (oldVal != null)

  8. return oldVal;

  9. break;

  10. }

  11. ...

  12. }

  13.  
 
  1. private final void treeifyBin(Node<K,V>[] tab, int index) {

  2. Node<K,V> b; int n, sc;

  3. if (tab != null) {

  4. //数组的大小还未超过64

  5. if ((n = tab.length) < MIN_TREEIFY_CAPACITY)

  6. //扩容

  7. tryPresize(n << 1);

  8. else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {

  9. //转换成红黑树

  10. ...

  11. }

  12. }

  13. }

3 当发现其他线程扩容时,帮其扩容

 
  1. final V putVal(K key, V value, boolean onlyIfAbsent) {

  2. ...

  3. //f.hash == MOVED 表示为:ForwardingNode,说明其他线程正在扩容

  4. else if ((fh = f.hash) == MOVED)

  5. tab = helpTransfer(tab, f);

  6. ...

  7. }

  8.  

扩容过程分析

  1. 线程执行put操作,发现容量已经达到扩容阈值,需要进行扩容操作,此时transferindex=tab.length=32

image.png

  1. 扩容线程A 以cas的方式修改transferindex=31-16=16 ,然后按照降序迁移table[31]--table[16]这个区间的hash桶

image.png

  1. 迁移hash桶时,会将桶内的链表或者红黑树,按照一定算法,拆分成2份,将其插入nextTable[i]和nextTable[i+n](n是table数组的长度)。 迁移完毕的hash桶,会被设置成ForwardingNode节点,以此告知访问此桶的其他线程,此节点已经迁移完毕。

image.png

相关代码如下:

 
  1.  
  2. private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {

  3. ...//省略无关代码

  4. synchronized (f) {

  5. //将node链表,分成2个新的node链表

  6. for (Node<K,V> p = f; p != lastRun; p = p.next) {

  7. int ph = p.hash; K pk = p.key; V pv = p.val;

  8. if ((ph & n) == 0)

  9. ln = new Node<K,V>(ph, pk, pv, ln);

  10. else

  11. hn = new Node<K,V>(ph, pk, pv, hn);

  12. }

  13. //将新node链表赋给nextTab

  14. setTabAt(nextTab, i, ln);

  15. setTabAt(nextTab, i + n, hn);

  16. setTabAt(tab, i, fwd);

  17. }

  18. ...//省略无关代码

  19. }

  1. 此时线程2访问到了ForwardingNode节点,如果线程2执行的put或remove等写操作,那么就会先帮其扩容。如果线程2执行的是get等读方法,则会调用ForwardingNode的find方法,去nextTable里面查找相关元素。

image.png

  1. 线程2加入扩容操作

image.png

  1. 如果准备加入扩容的线程,发现以下情况,放弃扩容,直接返回。
    • 发现transferIndex=0,即所有node均已分配
    • 发现扩容线程已经达到最大扩容线程数

image.png

部分源码分析

tryPresize方法

协调多个线程如何调用transfer方法进行hash桶的迁移(addCount,helpTransfer 方法中也有类似的逻辑)

 
  1. private final void tryPresize(int size) {

  2. //计算扩容的目标size

  3. int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :

  4. tableSizeFor(size + (size >>> 1) + 1);

  5. int sc;

  6. while ((sc = sizeCtl) >= 0) {

  7. Node<K,V>[] tab = table; int n;

  8. //tab没有初始化

  9. if (tab == null || (n = tab.length) == 0) {

  10. n = (sc > c) ? sc : c;

  11. //初始化之前,CAS设置sizeCtl=-1

  12. if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

  13. try {

  14. if (table == tab) {

  15. @SuppressWarnings("unchecked")

  16. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

  17. table = nt;

  18. //sc=0.75n,相当于扩容阈值

  19. sc = n - (n >>> 2);

  20. }

  21. } finally {

  22. //此时并没有通过CAS赋值,因为其他想要执行初始化的线程,发现sizeCtl=-1,就直接返回,从而确保任何情况,只会有一个线程执行初始化操作。

  23. sizeCtl = sc;

  24. }

  25. }

  26. }

  27. //目标扩容size小于扩容阈值,或者容量超过最大限制时,不需要扩容

  28. else if (c <= sc || n >= MAXIMUM_CAPACITY)

  29. break;

  30. //扩容

  31. else if (tab == table) {

  32. int rs = resizeStamp(n);

  33. //sc<0表示,已经有其他线程正在扩容

  34. if (sc < 0) {

  35. Node<K,V>[] nt;

  36. /**

  37. 1 (sc >>> RESIZE_STAMP_SHIFT) != rs :扩容线程数 > MAX_RESIZERS-1

  38. 2 sc == rs + 1 和 sc == rs + MAX_RESIZERS :表示什么???

  39. 3 (nt = nextTable) == null :表示nextTable正在初始化

  40. 4 transferIndex <= 0 :表示所有hash桶均分配出去

  41. */

  42. //如果不需要帮其扩容,直接返回

  43. if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||

  44. sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||

  45. transferIndex <= 0)

  46. break;

  47. //CAS设置sizeCtl=sizeCtl+1

  48. if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))

  49. //帮其扩容

  50. transfer(tab, nt);

  51. }

  52. //第一个执行扩容操作的线程,将sizeCtl设置为:(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)

  53. else if (U.compareAndSwapInt(this, SIZECTL, sc,

  54. (rs << RESIZE_STAMP_SHIFT) + 2))

  55. transfer(tab, null);

  56. }

  57. }

  58. }

transfer方法

负责迁移node节点

 
  1.  
  2. private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {

  3. int n = tab.length, stride;

  4. //计算需要迁移多少个hash桶(MIN_TRANSFER_STRIDE该值作为下限,以避免扩容线程过多)

  5. if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)

  6. stride = MIN_TRANSFER_STRIDE; // subdivide range

  7.  
  8. if (nextTab == null) { // initiating

  9. try {

  10. //扩容一倍

  11. @SuppressWarnings("unchecked")

  12. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];

  13. nextTab = nt;

  14. } catch (Throwable ex) { // try to cope with OOME

  15. sizeCtl = Integer.MAX_VALUE;

  16. return;

  17. }

  18. nextTable = nextTab;

  19. transferIndex = n;

  20. }

  21. int nextn = nextTab.length;

  22. ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

  23. boolean advance = true;

  24. boolean finishing = false; // to ensure sweep before committing nextTab

  25.  
  26. //1 逆序迁移已经获取到的hash桶集合,如果迁移完毕,则更新transferIndex,获取下一批待迁移的hash桶

  27. //2 如果transferIndex=0,表示所以hash桶均被分配,将i置为-1,准备退出transfer方法

  28. for (int i = 0, bound = 0;;) {

  29. Node<K,V> f; int fh;

  30.  
  31. //更新待迁移的hash桶索引

  32. while (advance) {

  33. int nextIndex, nextBound;

  34. //更新迁移索引i。

  35. if (--i >= bound || finishing)

  36. advance = false;

  37. else if ((nextIndex = transferIndex) <= 0) {

  38. //transferIndex<=0表示已经没有需要迁移的hash桶,将i置为-1,线程准备退出

  39. i = -1;

  40. advance = false;

  41. }

  42. //当迁移完bound这个桶后,尝试更新transferIndex,,获取下一批待迁移的hash桶

  43. else if (U.compareAndSwapInt

  44. (this, TRANSFERINDEX, nextIndex,

  45. nextBound = (nextIndex > stride ?

  46. nextIndex - stride : 0))) {

  47. bound = nextBound;

  48. i = nextIndex - 1;

  49. advance = false;

  50. }

  51. }

  52. //退出transfer

  53. if (i < 0 || i >= n || i + n >= nextn) {

  54. int sc;

  55. if (finishing) {

  56. //最后一个迁移的线程,recheck后,做收尾工作,然后退出

  57. nextTable = null;

  58. table = nextTab;

  59. sizeCtl = (n << 1) - (n >>> 1);

  60. return;

  61. }

  62. if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {

  63. /**

  64. 第一个扩容的线程,执行transfer方法之前,会设置 sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)

  65. 后续帮其扩容的线程,执行transfer方法之前,会设置 sizeCtl = sizeCtl+1

  66. 每一个退出transfer的方法的线程,退出之前,会设置 sizeCtl = sizeCtl-1

  67. 那么最后一个线程退出时:

  68. 必然有sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT

  69. */

  70.  
  71. //不相等,说明不到最后一个线程,直接退出transfer方法

  72. if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)

  73. return;

  74. finishing = advance = true;

  75. //最后退出的线程要重新check下是否全部迁移完毕

  76. i = n; // recheck before commit

  77. }

  78. }

  79. else if ((f = tabAt(tab, i)) == null)

  80. advance = casTabAt(tab, i, null, fwd);

  81. else if ((fh = f.hash) == MOVED)

  82. advance = true; // already processed

  83. //迁移node节点

  84. else {

  85. synchronized (f) {

  86. if (tabAt(tab, i) == f) {

  87. Node<K,V> ln, hn;

  88. //链表迁移

  89. if (fh >= 0) {

  90. int runBit = fh & n;

  91. Node<K,V> lastRun = f;

  92. for (Node<K,V> p = f.next; p != null; p = p.next) {

  93. int b = p.hash & n;

  94. if (b != runBit) {

  95. runBit = b;

  96. lastRun = p;

  97. }

  98. }

  99. if (runBit == 0) {

  100. ln = lastRun;

  101. hn = null;

  102. }

  103. else {

  104. hn = lastRun;

  105. ln = null;

  106. }

  107. //将node链表,分成2个新的node链表

  108. for (Node<K,V> p = f; p != lastRun; p = p.next) {

  109. int ph = p.hash; K pk = p.key; V pv = p.val;

  110. if ((ph & n) == 0)

  111. ln = new Node<K,V>(ph, pk, pv, ln);

  112. else

  113. hn = new Node<K,V>(ph, pk, pv, hn);

  114. }

  115. //将新node链表赋给nextTab

  116. setTabAt(nextTab, i, ln);

  117. setTabAt(nextTab, i + n, hn);

  118. setTabAt(tab, i, fwd);

  119. advance = true;

  120. }

  121. //红黑树迁移

  122. else if (f instanceof TreeBin) {

  123. TreeBin<K,V> t = (TreeBin<K,V>)f;

  124. TreeNode<K,V> lo = null, loTail = null;

  125. TreeNode<K,V> hi = null, hiTail = null;

  126. int lc = 0, hc = 0;

  127. for (Node<K,V> e = t.first; e != null; e = e.next) {

  128. int h = e.hash;

  129. TreeNode<K,V> p = new TreeNode<K,V>

  130. (h, e.key, e.val, null, null);

  131. if ((h & n) == 0) {

  132. if ((p.prev = loTail) == null)

  133. lo = p;

  134. else

  135. loTail.next = p;

  136. loTail = p;

  137. ++lc;

  138. }

  139. else {

  140. if ((p.prev = hiTail) == null)

  141. hi = p;

  142. else

  143. hiTail.next = p;

  144. hiTail = p;

  145. ++hc;

  146. }

  147. }

  148. ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :

  149. (hc != 0) ? new TreeBin<K,V>(lo) : t;

  150. hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :

  151. (lc != 0) ? new TreeBin<K,V>(hi) : t;

  152. setTabAt(nextTab, i, ln);

  153. setTabAt(nextTab, i + n, hn);

  154. setTabAt(tab, i, fwd);

  155. advance = true;

  156. }

  157. }

  158. }

  159. }

  160. }

  161. }

总结

多线程无锁扩容的关键就是通过CAS设置sizeCtl与transferIndex变量,协调多个线程对table数组中的node进行迁移。

参考资料

http://www.jianshu.com/p/f6730d5784ad




参考链接:https://www.jianshu.com/p/487d00afe6ca
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

那些年的代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值