ConcurrentHashMap transfer分析·

清疚 2022-12-12 04:53 245阅读 0赞

文章目录

  • 前言
  • transfer初始化
  • transfer哈希桶范围确定
  • transfer 拷贝旧数据到新的哈希表
  • 参考文献

前言

本文分析1.8之下的源码。本文需要读者之前对HashMap有一定了解,如HashMap中的红黑树和链表等。

我们回顾下HashMap基本结构:

在这里插入图片描述

ConcurrentHashMap的扩容算法极其精妙,也是最晦涩难懂的部分.

我首先将代码分段梳理各个部分的功能,在做细节说明。

  1. //ConcurrentHashMap.java
  2. private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  3. int n = tab.length, stride;
  4. //stride 决定一个线程处理的哈希表中多少个位置.比如为10那么处理,10个哈希桶的数据迁移工作
  5. if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
  6. stride = MIN_TRANSFER_STRIDE; // subdivide range
  7. if (nextTab == null) {
  8. // initiating
  9. //(1)构造一个新的数组容量为旧数据的两倍
  10. }
  11. int nextn = nextTab.length;
  12. ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
  13. boolean advance = true;
  14. boolean finishing = false; // to ensure sweep before committing nextTab
  15. for (int i = 0, bound = 0;;) {
  16. Node<K,V> f; int fh;
  17. while (advance) {
  18. //这里作用使用确定 i和bound的数值。
  19. //i和bound决定当前线程处理哈希表中 从i到bound位置的哈希桶拷贝到新表
  20. }
  21. if (i < 0 || i >= n || i + n >= nextn) {
  22. //判断是否结束
  23. }
  24. else if ((f = tabAt(tab, i)) == null)
  25. advance = casTabAt(tab, i, null, fwd);
  26. else if ((fh = f.hash) == MOVED)
  27. advance = true;
  28. else {
  29. synchronized (f) {
  30. if (tabAt(tab, i) == f) {
  31. Node<K,V> ln, hn;
  32. if (fh >= 0) {
  33. //链表哈希桶 迁移到新的扩容数组。
  34. }
  35. else if (f instanceof TreeBin) {
  36. //红黑树哈希桶 迁移到新的扩容数组。本文只分析链表情况
  37. }
  38. }
  39. }
  40. }
  41. }
  42. }

transfer初始化

此部分代码用于创建一个新的hash表,和确定每个线程处理hash表最大长度。(比如处理3个哈希桶的数据拷贝到新表)

  1. //ConcurrentHashMap.java
  2. //forwarding nodes节点的哈希值
  3. static final int MOVED = -1; // hash for forwarding nodes
  4. //仅当扩容时不为空,其他时候都为空。扩容完成自动为null
  5. private transient volatile Node<K,V>[] nextTable;
  6. /**
  7. * 当扩容时nextTable分割的位置(+1)
  8. * The next table index (plus one) to split while resizing.
  9. */
  10. private transient volatile int transferIndex;
  11. //入参:tab 旧的哈希表
  12. //入参:nextTab 要么传入ConcurrentHashMap的nextTable属性,要么和参入null。这个参数表示构造的新哈希表
  13. private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  14. //旧表的长度
  15. int n = tab.length;
  16. //决定一个线程最大处理的哈希桶数量
  17. int stride;
  18. //NCPU表示cpu数量。
  19. //MIN_TRANSFER_STRIDE 数值为16
  20. //(n >>> 3)表示旧表长度除以8。至于为什么要将表除以8我就不得而知,
  21. //笔者认为是Doug Lea的实践经验除以8可以更高效。
  22. //综上以及结合代码此处:max( 旧表长度/8/cpu数量 ,MIN_TRANSFER_STRIDE)
  23. if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
  24. stride = MIN_TRANSFER_STRIDE; // subdivide range
  25. //新哈希表为空那么创建一个新的哈希表
  26. if (nextTab == null) {
  27. // initiating
  28. try {
  29. //构造一个容量为旧表两倍的数组。n << 1表示为旧表长度*2
  30. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
  31. nextTab = nt;
  32. } catch (Throwable ex) {
  33. // try to cope with OOME
  34. sizeCtl = Integer.MAX_VALUE;
  35. return;
  36. }
  37. //赋值给成员属性,防止在多线程下多次初始化
  38. nextTable = nextTab;
  39. //设置成员属性
  40. transferIndex = n;
  41. }
  42. //新哈希表长度
  43. int nextn = nextTab.length;
  44. //占位节点。并且哈希值为-1,对应为成员字段MOVED
  45. ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
  46. //...略
  47. }

总结就是构造了一个新的哈希表并设置了最大处理长度.

transfer哈希桶范围确定

  1. //ConcurrentHashMap.java
  2. //入参:tab 旧的哈希表
  3. //入参:nextTab 要么传入ConcurrentHashMap的nextTable属性,要么和参入null。这个参数表示构造的新哈希表
  4. private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  5. //...初始化代码略
  6. //...
  7. boolean advance = true;
  8. boolean finishing = false; // to ensure sweep before committing nextTab
  9. //for循环就是用来做旧表迁移到新表的逻辑
  10. //死循环。直到迁移完成。i表示当前线程处理哈希桶的初始下标,bound表示当前显示线程处理哈希桶的结束下标
  11. for (int i = 0, bound = 0;;) {
  12. Node<K,V> f;
  13. int fh;
  14. //
  15. while (advance) {
  16. int nextIndex;
  17. int nextBound;
  18. //--i的寓意 是在确定bound和i之后的下次循环才有意义
  19. //这里--i 是表示处理区间的下一个哈希桶元素列表,因为上次已经处理过i位置。
  20. if (--i >= bound || finishing)
  21. advance = false;
  22. else if ((nextIndex = transferIndex) <= 0) {
  23. //如果下标不合法就结束
  24. i = -1;
  25. advance = false;
  26. }
  27. //cas方式设置ConcurrentHashMap的transferIndex属性
  28. //transferIndex在transfer初始化阶段赋值为旧哈希表长度
  29. //nextIndex此时等于transferIndex。
  30. // nextBound = (nextIndex > stride ? nextIndex - stride : 0))
  31. // nextIndex > stride 如果当前分割点位置(transferIndex)大于最大处理阈值stride
  32. //就返回nextIndex - stride 作为处理哈希桶区间的起始下标,否则是0
  33. else if (U.compareAndSwapInt
  34. (this, TRANSFERINDEX, nextIndex,
  35. nextBound = (nextIndex > stride ?
  36. nextIndex - stride : 0))) {
  37. //确定起始下标和终止下标
  38. bound = nextBound;
  39. i = nextIndex - 1;
  40. advance = false;
  41. }
  42. }
  43. //...略
  44. }//for
  45. }

总结 : 确定当前要处理哪部分哈希桶.bound起始坐标,i结束坐标。

transfer 拷贝旧数据到新的哈希表

  1. private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  2. //...
  3. //..初始化新的哈希表
  4. //略
  5. //固定哈希值为MOVE,用于标识正在扩容
  6. ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
  7. //此for循环用于拷贝旧哈希表数据到新的哈希表
  8. for (int i = 0, bound = 0;;) {
  9. Node<K,V> f;
  10. int fh;
  11. while (advance) {
  12. //确定bound和i数值 然后 advance=false 略
  13. }
  14. //i < 0 数据不合法
  15. // i >= n 当前处理的范围大于旧链表最大长度,已经不需要拷贝越界数据
  16. // i + n >= nextn 。nextn表示新哈希表长度,如果当前长度超过新哈希表长度,
  17. //证明是不合法的
  18. if (i < 0 || i >= n || i + n >= nextn) {
  19. int sc;
  20. if (finishing) {
  21. nextTable = null;
  22. table = nextTab;
  23. sizeCtl = (n << 1) - (n >>> 1);
  24. return;
  25. }
  26. if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
  27. if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
  28. return;
  29. finishing = advance = true;
  30. i = n; // recheck before commit
  31. }
  32. }
  33. //如果旧的哈希表第i个位置不存在元素那么,放置一个fwd对象,且hash值为MOVED
  34. //可以让其他插入线程感知
  35. else if ((f = tabAt(tab, i)) == null)
  36. advance = casTabAt(tab, i, null, fwd);
  37. else if ((fh = f.hash) == MOVED)
  38. //当前位置已经被其他线程替换成fwd对象,那么重新拷贝其他哈希桶位置到新哈希表
  39. //advance标志为true 让其重新选择哈希桶拷贝到新哈希表
  40. advance = true; // already processed
  41. else {
  42. //else负责移动哈希桶元素到新的哈希表
  43. //所有哈希桶移动删除必须加锁头节点
  44. synchronized (f) {
  45. //需要二次判断 当前哈希桶头结点是否改变,因为在获取到锁时
  46. //哈希桶头结点已经改变了
  47. if (tabAt(tab, i) == f) {
  48. //用于移动哈希桶元素到新的低地址元素
  49. Node<K,V> ln;
  50. //用于移动哈希桶元素到新的高地址元素
  51. Node<K,V> hn;
  52. //如果哈希值大于0证明哈希桶是链表结构
  53. if (fh >= 0) {
  54. //n表示旧的哈希表长度且为2的n次方(方便利用位运算得到哈希位置)
  55. //所以n的字节特点就是 只有一个1其余都是0.比如16二进制为1 0000
  56. //因此跟n做位运算的与预算 结果只能为 0或者为n。
  57. //如果fh & n 为n:
  58. //证明此元素在新哈希表中的 x+n 位置上。(x为当前哈希表数组下标所在位置)
  59. //对应下面代码 setTabAt(nextTab, i + n, hn);
  60. //如果 fh & n 为0 :
  61. //那么此元素在新的哈希表中的位置和当前在旧的哈希表位置下标是一样,
  62. //对应下面代码 setTabAt(nextTab, i, ln);
  63. //上述数理逻辑读者可自行思考。
  64. //逻辑不是太难理解,因为如果元素哈希值能于旧哈希表长度N按位与得到N,那么证明此元素在新哈希表长度M(为旧数组两倍M=2N)的下标必然大于n且为当前数组位置+n。
  65. //比如 旧哈希表长度 等于8 新数组长度为16,某个元素的哈希值为15
  66. //在旧的哈希表中:元素哈希对应数组下标为7。在新的数组中下标为15(旧数组下标+旧数组长度=7+8).
  67. int runBit = fh & n;
  68. //当前指向哈希桶头结点
  69. //在后面紧跟的for循环中改变指向为最后一个runBit不同的节点
  70. Node<K,V> lastRun = f;
  71. //这里目的是不知道怎么用言语去表述
  72. //寻找到一个全部后续节点哈希值和旧数组长度相同哈希结果相同的第一个节点
  73. //比如某个lastRun的p.hash & n为0,那么后续所有子元素都要求p.hash & n为0
  74. //或者说某个lastRun的p.hash & n为n,那么后续所有子元素都要求p.hash & n为n
  75. //这里这么做是方便移动哈希桶元素的到新哈希表的两种情况。(一种是保持位在新的哈希表不变,另一种是当前位置加旧哈希表长度)
  76. for (Node<K,V> p = f.next; p != null; p = p.next) {
  77. int b = p.hash & n;
  78. if (b != runBit) {
  79. runBit = b;
  80. lastRun = p;
  81. }
  82. }
  83. //判断lastRun是那种类型
  84. if (runBit == 0) {
  85. ln = lastRun;
  86. hn = null;
  87. }
  88. else {
  89. hn = lastRun;
  90. ln = null;
  91. }
  92. //移动哈希桶所有元素到新哈希表。
  93. //一种是下标不变移动
  94. //另一种是 当前下标在旧哈希长度
  95. for (Node<K,V> p = f; p != lastRun; p = p.next) {
  96. int ph = p.hash; K pk = p.key; V pv = p.val;
  97. if ((ph & n) == 0)
  98. ln = new Node<K,V>(ph, pk, pv, ln);
  99. else
  100. hn = new Node<K,V>(ph, pk, pv, hn);
  101. }
  102. setTabAt(nextTab, i, ln);
  103. setTabAt(nextTab, i + n, hn);
  104. setTabAt(tab, i, fwd);
  105. //设置advance为true让当前线程继续计算处理其他旧哈希桶位置的拷贝
  106. advance = true;
  107. }
  108. else if (f instanceof TreeBin) {
  109. //..红黑树部分略
  110. }
  111. }
  112. }
  113. }
  114. }
  115. }

任何哈希桶下插入删除的操作需要加锁说明:

情况一:
多个线程想同时插入新的节点到同一个哈希桶的链表中。
在这里插入图片描述
情况二:

一个线程插入哈希桶,另一个线程进行移除操作。
在这里插入图片描述
虽然上述操作可以使用CAS加自旋完成,但是会增加复杂度。

参考文献

  1. 并发编程——ConcurrentHashMap#transfer() 扩容逐行分析
  2. 并发容器之ConcurrentHashMap详解(JDK1.8版本)与源码分析
  3. HashMap位运算你可知一二

发表评论

表情:
评论列表 (有 0 条评论,245人围观)

还没有评论,来说两句吧...

相关阅读

    相关 ConcurrentHashMap原理分析

    一.Java并发基础 当一个对象或变量可以被多个线程共享的时候,就有可能使得程序的逻辑出现问题。 在一个对象中有一个变量i=0,有两个线程A,B都想对i加1,这个时候便有