注:本文源码是JDK8的版本,与之前的版本有较大差异

转载地址:http://blog.csdn.net/u010723709/article/details/48007881

ConcurrentHashMap是conccurrent家族中的一个类,由于它可以高效地支持并发操作,以及被广泛使用,经典的开源框架spring的底层数据结构就是使用ConcurrentHashMap实现的。与同是线程安全的老大哥HashTable相比,它已经更胜一筹,因此它的锁更加细化,而不是像HashTable一样为几乎每个方法都添加了synchronized锁,这样的锁无疑会影响到性能。

本文的分析的源码是JDK8的版本,与JDK6的版本有很大的差异。实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。

1 重要的属性

首先来看几个重要的属性,与HashMap相同的就不再介绍了,这里重点解释一下sizeCtl这个属性。可以说它是ConcurrentHashMap中出镜率很高的一个属性,因为它是一个控制标识符,在不同的地方有不同用途,而且它的取值不同,也代表不同的含义。

  • 负数代表正在进行初始化或扩容操作
  • -1代表正在初始化
  • -N 表示有N-1个线程正在进行扩容操作
  • 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。还后面可以看到,它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。
  1. /**
  2. * 盛装Node元素的数组 它的大小是2的整数次幂
  3. * Size is always a power of two. Accessed directly by iterators.
  4. */
  5. transient volatile Node<K,V>[] table;
  6. /**
  7. * Table initialization and resizing control.  When negative, the
  8. * table is being initialized or resized: -1 for initialization,
  9. * else -(1 + the number of active resizing threads).  Otherwise,
  10. * when table is null, holds the initial table size to use upon
  11. * creation, or 0 for default. After initialization, holds the
  12. * next element count value upon which to resize the table.
  13. hash表初始化或扩容时的一个控制位标识量。
  14. 负数代表正在进行初始化或扩容操作
  15. -1代表正在初始化
  16. -N 表示有N-1个线程正在进行扩容操作
  17. 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
  18. */
  19. private transient volatile int sizeCtl;
  20. // 以下两个是用来控制扩容的时候 单线程进入的变量
  21. /**
  22. * The number of bits used for generation stamp in sizeCtl.
  23. * Must be at least 6 for 32bit arrays.
  24. */
  25. private static int RESIZE_STAMP_BITS = 16;
  26. /**
  27. * The bit shift for recording size stamp in sizeCtl.
  28. */
  29. private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
  30. /*
  31. * Encodings for Node hash fields. See above for explanation.
  32. */
  33. static final int MOVED     = -1; // hash值是-1,表示这是一个forwardNode节点
  34. static final int TREEBIN   = -2; // hash值是-2  表示这时一个TreeBin节点

2 重要的内部类

2.1 Node

Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但是但是有一些差别它对value和next属性设置了volatile同步锁,它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。

  1. static class Node<K,V> implements Map.Entry<K,V> {
  2. final int hash;
  3. final K key;
  4. volatile V val;//带有同步锁的value
  5. volatile Node<K,V> next;//带有同步锁的next指针
  6. Node(int hash, K key, V val, Node<K,V> next) {
  7. this.hash = hash;
  8. this.key = key;
  9. this.val = val;
  10. this.next = next;
  11. }
  12. public final K getKey()       { return key; }
  13. public final V getValue()     { return val; }
  14. public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
  15. public final String toString(){ return key + "=" + val; }
  16. //不允许直接改变value的值
  17. public final V setValue(V value) {
  18. throw new UnsupportedOperationException();
  19. }
  20. public final boolean equals(Object o) {
  21. Object k, v, u; Map.Entry<?,?> e;
  22. return ((o instanceof Map.Entry) &&
  23. (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
  24. (v = e.getValue()) != null &&
  25. (k == key || k.equals(key)) &&
  26. (v == (u = val) || v.equals(u)));
  27. }
  28. /**
  29. * Virtualized support for map.get(); overridden in subclasses.
  30. */
  31. Node<K,V> find(int h, Object k) {
  32. Node<K,V> e = this;
  33. if (k != null) {
  34. do {
  35. K ek;
  36. if (e.hash == h &&
  37. ((ek = e.key) == k || (ek != null && k.equals(ek))))
  38. return e;
  39. } while ((e = e.next) != null);
  40. }
  41. return null;
  42. }
  43. }
  44. 这个Node内部类与HashMap中定义的Node类很相似,但是有一些差别
  45. 它对value和next属性设置了volatile同步锁
  46. 它不允许调用setValue方法直接改变Node的value域
  47. 它增加了find方法辅助map.get()方法

2.2 TreeNode

树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap集成自Node类,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。

2.3 TreeBin

这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。

这里仅贴出它的构造方法。可以看到在构造TreeBin节点时,仅仅指定了它的hash值为TREEBIN常量,这也就是个标识为。同时也看到我们熟悉的红黑树构造方法

  1. /**
  2. * Creates bin with initial set of nodes headed by b.
  3. */
  4. TreeBin(TreeNode<K,V> b) {
  5. super(TREEBIN, null, null, null);
  6. this.first = b;
  7. TreeNode<K,V> r = null;
  8. for (TreeNode<K,V> x = b, next; x != null; x = next) {
  9. next = (TreeNode<K,V>)x.next;
  10. x.left = x.right = null;
  11. if (r == null) {
  12. x.parent = null;
  13. x.red = false;
  14. r = x;
  15. }
  16. else {
  17. K k = x.key;
  18. int h = x.hash;
  19. Class<?> kc = null;
  20. for (TreeNode<K,V> p = r;;) {
  21. int dir, ph;
  22. K pk = p.key;
  23. if ((ph = p.hash) > h)
  24. dir = -1;
  25. else if (ph < h)
  26. dir = 1;
  27. else if ((kc == null &&
  28. (kc = comparableClassFor(k)) == null) ||
  29. (dir = compareComparables(kc, k, pk)) == 0)
  30. dir = tieBreakOrder(k, pk);
  31. TreeNode<K,V> xp = p;
  32. if ((p = (dir <= 0) ? p.left : p.right) == null) {
  33. x.parent = xp;
  34. if (dir <= 0)
  35. xp.left = x;
  36. else
  37. xp.right = x;
  38. r = balanceInsertion(r, x);
  39. break;
  40. }
  41. }
  42. }
  43. }
  44. this.root = r;
  45. assert checkInvariants(root);
  46. }

2.5 ForwardingNode

一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找

  1. /**
  2. * A node inserted at head of bins during transfer operations.
  3. */
  4. static final class ForwardingNode<K,V> extends Node<K,V> {
  5. final Node<K,V>[] nextTable;
  6. ForwardingNode(Node<K,V>[] tab) {
  7. super(MOVED, null, null, null);
  8. this.nextTable = tab;
  9. }
  10. Node<K,V> find(int h, Object k) {
  11. // loop to avoid arbitrarily deep recursion on forwarding nodes
  12. outer: for (Node<K,V>[] tab = nextTable;;) {
  13. Node<K,V> e; int n;
  14. if (k == null || tab == null || (n = tab.length) == 0 ||
  15. (e = tabAt(tab, (n - 1) & h)) == null)
  16. return null;
  17. for (;;) {
  18. int eh; K ek;
  19. if ((eh = e.hash) == h &&
  20. ((ek = e.key) == k || (ek != null && k.equals(ek))))
  21. return e;
  22. if (eh < 0) {
  23. if (e instanceof ForwardingNode) {
  24. tab = ((ForwardingNode<K,V>)e).nextTable;
  25. continue outer;
  26. }
  27. else
  28. return e.find(h, k);
  29. }
  30. if ((e = e.next) == null)
  31. return null;
  32. }
  33. }
  34. }
  35. }

3 Unsafe与CAS

在ConcurrentHashMap中,随处可以看到U, 大量使用了U.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁,SVN的思想是比较类似的。

3.1 unsafe静态块

unsafe代码块控制了一些属性的修改工作,比如最常用的SIZECTL 。  在这一版本的concurrentHashMap中,大量应用来的CAS方法进行变量、属性的修改工作。  利用CAS进行无锁操作,可以大大提高性能。

  1. private static final sun.misc.Unsafe U;
  2. private static final long SIZECTL;
  3. private static final long TRANSFERINDEX;
  4. private static final long BASECOUNT;
  5. private static final long CELLSBUSY;
  6. private static final long CELLVALUE;
  7. private static final long ABASE;
  8. private static final int ASHIFT;
  9. static {
  10. try {
  11. U = sun.misc.Unsafe.getUnsafe();
  12. Class<?> k = ConcurrentHashMap.class;
  13. SIZECTL = U.objectFieldOffset
  14. (k.getDeclaredField("sizeCtl"));
  15. TRANSFERINDEX = U.objectFieldOffset
  16. (k.getDeclaredField("transferIndex"));
  17. BASECOUNT = U.objectFieldOffset
  18. (k.getDeclaredField("baseCount"));
  19. CELLSBUSY = U.objectFieldOffset
  20. (k.getDeclaredField("cellsBusy"));
  21. Class<?> ck = CounterCell.class;
  22. CELLVALUE = U.objectFieldOffset
  23. (ck.getDeclaredField("value"));
  24. Class<?> ak = Node[].class;
  25. ABASE = U.arrayBaseOffset(ak);
  26. int scale = U.arrayIndexScale(ak);
  27. if ((scale & (scale - 1)) != 0)
  28. throw new Error("data type scale not a power of two");
  29. ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
  30. } catch (Exception e) {
  31. throw new Error(e);
  32. }
  33. }

3.2 三个核心方法

ConcurrentHashMap定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全。

  1. @SuppressWarnings("unchecked")
  2. //获得在i位置上的Node节点
  3. static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
  4. return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
  5. }
  6. //利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
  7. //在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改
  8. //因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果  有点类似于SVN
  9. static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
  10. Node<K,V> c, Node<K,V> v) {
  11. return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
  12. }
  13. //利用volatile方法设置节点位置的值
  14. static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
  15. U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
  16. }

4 初始化方法initTable

对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置了一些参数而已。而整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的。如调用put、computeIfAbsent、compute、merge等方法的时候,调用时机是检查table==null。

初始化方法主要应用了关键属性sizeCtl 如果这个值〈0,表示其他线程正在进行初始化,就放弃这个操作。在这也可以看出ConcurrentHashMap的初始化只能由一个线程完成。如果获得了初始化权限,就用CAS方法将sizeCtl置为-1,防止其他线程进入。初始化数组后,将sizeCtl的值改为0.75*n

  1. /**
  2. * Initializes table, using the size recorded in sizeCtl.
  3. */
  4. private final Node<K,V>[] initTable() {
  5. Node<K,V>[] tab; int sc;
  6. while ((tab = table) == null || tab.length == 0) {
  7. //sizeCtl表示有其他线程正在进行初始化操作,把线程挂起。对于table的初始化工作,只能有一个线程在进行。
  8. if ((sc = sizeCtl) < 0)
  9. Thread.yield(); // lost initialization race; just spin
  10. else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//利用CAS方法把sizectl的值置为-1 表示本线程正在进行初始化
  11. try {
  12. if ((tab = table) == null || tab.length == 0) {
  13. int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
  14. @SuppressWarnings("unchecked")
  15. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
  16. table = tab = nt;
  17. sc = n - (n >>> 2);//相当于0.75*n 设置一个扩容的阈值
  18. }
  19. } finally {
  20. sizeCtl = sc;
  21. }
  22. break;
  23. }
  24. }
  25. return tab;
  26. }

5 扩容方法 transfer

当ConcurrentHashMap容量不足的时候,需要对table进行扩容。这个方法的基本思想跟HashMap是很像的,但是由于它是支持并发扩容的,所以要复杂的多。原因是它支持多线程进行扩容操作,而并没有加锁。我想这样做的目的不仅仅是为了满足concurrent的要求,而是希望利用并发处理去减少扩容带来的时间影响。因为在扩容的时候,总是会涉及到从一个“数组”到另一个“数组”拷贝的操作,如果这个操作能够并发进行,那真真是极好的了。

整个扩容操作分为两个部分

  • 第一部分是构建一个nextTable,它的容量是原来的两倍,这个操作是单线程完成的。这个单线程的保证是通过RESIZE_STAMP_SHIFT这个常量经过一次运算来保证的,这个地方在后面会有提到;

  • 第二个部分就是将原来table中的元素复制到nextTable中,这里允许多线程进行操作。

先来看一下单线程是如何完成的:

它的大体思想就是遍历、复制的过程。首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素:

  • 如果这个位置为空,就在原table中的i位置放入forwardNode节点,这个也是触发并发扩容的关键点;

  • 如果这个位置是Node节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上

  • 如果这个位置是TreeBin节点(fh<0),也做一个反序处理,并且判断是否需要untreefi,把处理的结果分别放在nextTable的i和i+n的位置上

  • 遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的0.75倍 ,完成扩容。

再看一下多线程是如何完成的:

在代码的69行有一个判断,如果遍历到的节点是forward节点,就向后继续遍历,再加上给节点上锁的机制,就完成了多线程的控制。多线程遍历节点,处理了一个节点,就把对应点的值set为forward,另一个线程看到forward,就向后遍历。这样交叉就完成了复制工作。而且还很好的解决了线程安全的问题。 这个方法的设计实在是让我膜拜。

  1. /**
  2. * 一个过渡的table表  只有在扩容的时候才会使用
  3. */
  4. private transient volatile Node<K,V>[] nextTable;
  5. /**
  6. * Moves and/or copies the nodes in each bin to new table. See
  7. * above for explanation.
  8. */
  9. private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  10. int n = tab.length, stride;
  11. if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
  12. stride = MIN_TRANSFER_STRIDE; // subdivide range
  13. if (nextTab == null) {            // initiating
  14. try {
  15. @SuppressWarnings("unchecked")
  16. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//构造一个nextTable对象 它的容量是原来的两倍
  17. nextTab = nt;
  18. } catch (Throwable ex) {      // try to cope with OOME
  19. sizeCtl = Integer.MAX_VALUE;
  20. return;
  21. }
  22. nextTable = nextTab;
  23. transferIndex = n;
  24. }
  25. int nextn = nextTab.length;
  26. ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);//构造一个连节点指针 用于标志位
  27. boolean advance = true;//并发扩容的关键属性 如果等于true 说明这个节点已经处理过
  28. boolean finishing = false; // to ensure sweep before committing nextTab
  29. for (int i = 0, bound = 0;;) {
  30. Node<K,V> f; int fh;
  31. //这个while循环体的作用就是在控制i--  通过i--可以依次遍历原hash表中的节点
  32. while (advance) {
  33. int nextIndex, nextBound;
  34. if (--i >= bound || finishing)
  35. advance = false;
  36. else if ((nextIndex = transferIndex) <= 0) {
  37. i = -1;
  38. advance = false;
  39. }
  40. else if (U.compareAndSwapInt
  41. (this, TRANSFERINDEX, nextIndex,
  42. nextBound = (nextIndex > stride ?
  43. nextIndex - stride : 0))) {
  44. bound = nextBound;
  45. i = nextIndex - 1;
  46. advance = false;
  47. }
  48. }
  49. if (i < 0 || i >= n || i + n >= nextn) {
  50. int sc;
  51. if (finishing) {
  52. //如果所有的节点都已经完成复制工作  就把nextTable赋值给table 清空临时对象nextTable
  53. nextTable = null;
  54. table = nextTab;
  55. sizeCtl = (n << 1) - (n >>> 1);//扩容阈值设置为原来容量的1.5倍  依然相当于现在容量的0.75倍
  56. return;
  57. }
  58. //利用CAS方法更新这个扩容阈值,在这里面sizectl值减一,说明新加入一个线程参与到扩容操作
  59. if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
  60. if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
  61. return;
  62. finishing = advance = true;
  63. i = n; // recheck before commit
  64. }
  65. }
  66. //如果遍历到的节点为空 则放入ForwardingNode指针
  67. else if ((f = tabAt(tab, i)) == null)
  68. advance = casTabAt(tab, i, null, fwd);
  69. //如果遍历到ForwardingNode节点  说明这个点已经被处理过了 直接跳过  这里是控制并发扩容的核心
  70. else if ((fh = f.hash) == MOVED)
  71. advance = true; // already processed
  72. else {
  73. //节点上锁
  74. synchronized (f) {
  75. if (tabAt(tab, i) == f) {
  76. Node<K,V> ln, hn;
  77. //如果fh>=0 证明这是一个Node节点
  78. if (fh >= 0) {
  79. int runBit = fh & n;
  80. //以下的部分在完成的工作是构造两个链表  一个是原链表  另一个是原链表的反序排列
  81. Node<K,V> lastRun = f;
  82. for (Node<K,V> p = f.next; p != null; p = p.next) {
  83. int b = p.hash & n;
  84. if (b != runBit) {
  85. runBit = b;
  86. lastRun = p;
  87. }
  88. }
  89. if (runBit == 0) {
  90. ln = lastRun;
  91. hn = null;
  92. }
  93. else {
  94. hn = lastRun;
  95. ln = null;
  96. }
  97. for (Node<K,V> p = f; p != lastRun; p = p.next) {
  98. int ph = p.hash; K pk = p.key; V pv = p.val;
  99. if ((ph & n) == 0)
  100. ln = new Node<K,V>(ph, pk, pv, ln);
  101. else
  102. hn = new Node<K,V>(ph, pk, pv, hn);
  103. }
  104. //在nextTable的i位置上插入一个链表
  105. setTabAt(nextTab, i, ln);
  106. //在nextTable的i+n的位置上插入另一个链表
  107. setTabAt(nextTab, i + n, hn);
  108. //在table的i位置上插入forwardNode节点  表示已经处理过该节点
  109. setTabAt(tab, i, fwd);
  110. //设置advance为true 返回到上面的while循环中 就可以执行i--操作
  111. advance = true;
  112. }
  113. //对TreeBin对象进行处理  与上面的过程类似
  114. else if (f instanceof TreeBin) {
  115. TreeBin<K,V> t = (TreeBin<K,V>)f;
  116. TreeNode<K,V> lo = null, loTail = null;
  117. TreeNode<K,V> hi = null, hiTail = null;
  118. int lc = 0, hc = 0;
  119. //构造正序和反序两个链表
  120. for (Node<K,V> e = t.first; e != null; e = e.next) {
  121. int h = e.hash;
  122. TreeNode<K,V> p = new TreeNode<K,V>
  123. (h, e.key, e.val, null, null);
  124. if ((h & n) == 0) {
  125. if ((p.prev = loTail) == null)
  126. lo = p;
  127. else
  128. loTail.next = p;
  129. loTail = p;
  130. ++lc;
  131. }
  132. else {
  133. if ((p.prev = hiTail) == null)
  134. hi = p;
  135. else
  136. hiTail.next = p;
  137. hiTail = p;
  138. ++hc;
  139. }
  140. }
  141. //如果扩容后已经不再需要tree的结构 反向转换为链表结构
  142. ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
  143. (hc != 0) ? new TreeBin<K,V>(lo) : t;
  144. hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
  145. (lc != 0) ? new TreeBin<K,V>(hi) : t;
  146. //在nextTable的i位置上插入一个链表
  147. setTabAt(nextTab, i, ln);
  148. //在nextTable的i+n的位置上插入另一个链表
  149. setTabAt(nextTab, i + n, hn);
  150. //在table的i位置上插入forwardNode节点  表示已经处理过该节点
  151. setTabAt(tab, i, fwd);
  152. //设置advance为true 返回到上面的while循环中 就可以执行i--操作
  153. advance = true;
  154. }
  155. }
  156. }
  157. }
  158. }
  159. }

6 Put方法

前面的所有的介绍其实都为这个方法做铺垫。ConcurrentHashMap最常用的就是put和get两个方法。现在来介绍put方法,这个put方法依然沿用HashMap的put方法的思想,根据hash值计算这个新插入的点在table中的位置i,如果i位置是空的,直接放进去,否则进行判断,如果i位置是树节点,按照树的方式插入新的节点,否则把i插入到链表的末尾。ConcurrentHashMap中依然沿用这个思想,有一个最重要的不同点就是ConcurrentHashMap不允许key或value为null值。另外由于涉及到多线程,put方法就要复杂一点。在多线程中可能有以下两个情况

  1. 如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点,如果检测到需要插入的位置被forward节点占有,就帮助进行扩容;

  2. 如果检测到要插入的节点是非空且不是forward节点,就对这个节点加锁,这样就保证了线程安全。尽管这个有一些影响效率,但是还是会比hashTable的synchronized要好得多。

整体流程就是首先定义不允许key或value为null的情况放入  对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在table中的位置。

如果这个位置是空的,那么直接放入,而且不需要加锁操作。

如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到hash值与key值都与新加入节点是一致的情况,则只需要更新value值即可。否则依次向后遍历,直到链表尾插入这个结点。  如果加入这个节点以后链表长度大于8,就把这个链表转换成红黑树。如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。

  1. public V put(K key, V value) {
  2. return putVal(key, value, false);
  3. }
  4. /** Implementation for put and putIfAbsent */
  5. final V putVal(K key, V value, boolean onlyIfAbsent) {
  6. //不允许 key或value为null
  7. if (key == null || value == null) throw new NullPointerException();
  8. //计算hash值
  9. int hash = spread(key.hashCode());
  10. int binCount = 0;
  11. //死循环 何时插入成功 何时跳出
  12. for (Node<K,V>[] tab = table;;) {
  13. Node<K,V> f; int n, i, fh;
  14. //如果table为空的话,初始化table
  15. if (tab == null || (n = tab.length) == 0)
  16. tab = initTable();
  17. //根据hash值计算出在table里面的位置
  18. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
  19. //如果这个位置没有值 ,直接放进去,不需要加锁
  20. if (casTabAt(tab, i, null,
  21. new Node<K,V>(hash, key, value, null)))
  22. break;                   // no lock when adding to empty bin
  23. }
  24. //当遇到表连接点时,需要进行整合表的操作
  25. else if ((fh = f.hash) == MOVED)
  26. tab = helpTransfer(tab, f);
  27. else {
  28. V oldVal = null;
  29. //结点上锁  这里的结点可以理解为hash值相同组成的链表的头结点
  30. synchronized (f) {
  31. if (tabAt(tab, i) == f) {
  32. //fh〉0 说明这个节点是一个链表的节点 不是树的节点
  33. if (fh >= 0) {
  34. binCount = 1;
  35. //在这里遍历链表所有的结点
  36. for (Node<K,V> e = f;; ++binCount) {
  37. K ek;
  38. //如果hash值和key值相同  则修改对应结点的value值
  39. if (e.hash == hash &&
  40. ((ek = e.key) == key ||
  41. (ek != null && key.equals(ek)))) {
  42. oldVal = e.val;
  43. if (!onlyIfAbsent)
  44. e.val = value;
  45. break;
  46. }
  47. Node<K,V> pred = e;
  48. //如果遍历到了最后一个结点,那么就证明新的节点需要插入 就把它插入在链表尾部
  49. if ((e = e.next) == null) {
  50. pred.next = new Node<K,V>(hash, key,
  51. value, null);
  52. break;
  53. }
  54. }
  55. }
  56. //如果这个节点是树节点,就按照树的方式插入值
  57. else if (f instanceof TreeBin) {
  58. Node<K,V> p;
  59. binCount = 2;
  60. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
  61. value)) != null) {
  62. oldVal = p.val;
  63. if (!onlyIfAbsent)
  64. p.val = value;
  65. }
  66. }
  67. }
  68. }
  69. if (binCount != 0) {
  70. //如果链表长度已经达到临界值8 就需要把链表转换为树结构
  71. if (binCount >= TREEIFY_THRESHOLD)
  72. treeifyBin(tab, i);
  73. if (oldVal != null)
  74. return oldVal;
  75. break;
  76. }
  77. }
  78. }
  79. //将当前ConcurrentHashMap的元素数量+1
  80. addCount(1L, binCount);
  81. return null;
  82. }

6.1 helpTransfer方法

这是一个协助扩容的方法。这个方法被调用的时候,当前ConcurrentHashMap一定已经有了nextTable对象,首先拿到这个nextTable对象,调用transfer方法。回看上面的transfer方法可以看到,当本线程进入扩容方法的时候会直接进入复制阶段。

  1. /**
  2. * Helps transfer if a resize is in progress.
  3. */
  4. final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
  5. Node<K,V>[] nextTab; int sc;
  6. if (tab != null && (f instanceof ForwardingNode) &&
  7. (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
  8. int rs = resizeStamp(tab.length);//计算一个操作校验码
  9. while (nextTab == nextTable && table == tab &&
  10. (sc = sizeCtl) < 0) {
  11. if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
  12. sc == rs + MAX_RESIZERS || transferIndex <= 0)
  13. break;
  14. if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
  15. transfer(tab, nextTab);
  16. break;
  17. }
  18. }
  19. return nextTab;
  20. }
  21. return table;
  22. }

6.2 treeifyBin方法

这个方法用于将过长的链表转换为TreeBin对象。但是他并不是直接转换,而是进行一次容量判断,如果容量没有达到转换的要求,直接进行扩容操作并返回;如果满足条件才链表的结构抓换为TreeBin ,这与HashMap不同的是,它并没有把TreeNode直接放入红黑树,而是利用了TreeBin这个小容器来封装所有的TreeNode.

  1. private final void treeifyBin(Node<K,V>[] tab, int index) {
  2. Node<K,V> b; int n, sc;
  3. if (tab != null) {
  4. if ((n = tab.length) < MIN_TREEIFY_CAPACITY)//如果table.length<64 就扩大一倍 返回
  5. tryPresize(n << 1);
  6. else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
  7. synchronized (b) {
  8. if (tabAt(tab, index) == b) {
  9. TreeNode<K,V> hd = null, tl = null;
  10. //构造了一个TreeBin对象 把所有Node节点包装成TreeNode放进去
  11. for (Node<K,V> e = b; e != null; e = e.next) {
  12. TreeNode<K,V> p =
  13. new TreeNode<K,V>(e.hash, e.key, e.val,
  14. null, null);//这里只是利用了TreeNode封装 而没有利用TreeNode的next域和parent域
  15. if ((p.prev = tl) == null)
  16. hd = p;
  17. else
  18. tl.next = p;
  19. tl = p;
  20. }
  21. //在原来index的位置 用TreeBin替换掉原来的Node对象
  22. setTabAt(tab, index, new TreeBin<K,V>(hd));
  23. }
  24. }
  25. }
  26. }
  27. }

7 get方法

get方法比较简单,给定一个key来确定value的时候,必须满足两个条件  key相同  hash值相同,对于节点可能在链表或树上的情况,需要分别去查找.

  1. public V get(Object key) {
  2. Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
  3. //计算hash值
  4. int h = spread(key.hashCode());
  5. //根据hash值确定节点位置
  6. if ((tab = table) != null && (n = tab.length) > 0 &&
  7. (e = tabAt(tab, (n - 1) & h)) != null) {
  8. //如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点
  9. if ((eh = e.hash) == h) {
  10. if ((ek = e.key) == key || (ek != null && key.equals(ek)))
  11. return e.val;
  12. }
  13. //如果eh<0 说明这个节点在树上 直接寻找
  14. else if (eh < 0)
  15. return (p = e.find(h, key)) != null ? p.val : null;
  16. //否则遍历链表 找到对应的值并返回
  17. while ((e = e.next) != null) {
  18. if (e.hash == h &&
  19. ((ek = e.key) == key || (ek != null && key.equals(ek))))
  20. return e.val;
  21. }
  22. }
  23. return null;
  24. }

8 Size相关的方法

对于ConcurrentHashMap来说,这个table里到底装了多少东西其实是个不确定的数量,因为不可能在调用size()方法的时候像GC的“stop the world”一样让其他线程都停下来让你去统计,因此只能说这个数量是个估计值。对于这个估计值,ConcurrentHashMap也是大费周章才计算出来的。

8.1 辅助定义

为了统计元素个数,ConcurrentHashMap定义了一些变量和一个内部类

  1. /**
  2. * A padded cell for distributing counts.  Adapted from LongAdder
  3. * and Striped64.  See their internal docs for explanation.
  4. */
  5. @sun.misc.Contended static final class CounterCell {
  6. volatile long value;
  7. CounterCell(long x) { value = x; }
  8. }
  9. /******************************************/
  10. /**
  11. * 实际上保存的是hashmap中的元素个数  利用CAS锁进行更新
  12. 但它并不用返回当前hashmap的元素个数
  13. */
  14. private transient volatile long baseCount;
  15. /**
  16. * Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
  17. */
  18. private transient volatile int cellsBusy;
  19. /**
  20. * Table of counter cells. When non-null, size is a power of 2.
  21. */
  22. private transient volatile CounterCell[] counterCells;

8.2 mappingCount与Size方法

mappingCount与size方法的类似  从Java工程师给出的注释来看,应该使用mappingCount代替size方法 两个方法都没有直接返回basecount 而是统计一次这个值,而这个值其实也是一个大概的数值,因此可能在统计的时候有其他线程正在执行插入或删除操作。

  1. public int size() {
  2. long n = sumCount();
  3. return ((n < 0L) ? 0 :
  4. (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
  5. (int)n);
  6. }
  7. /**
  8. * Returns the number of mappings. This method should be used
  9. * instead of {@link #size} because a ConcurrentHashMap may
  10. * contain more mappings than can be represented as an int. The
  11. * value returned is an estimate; the actual count may differ if
  12. * there are concurrent insertions or removals.
  13. *
  14. * @return the number of mappings
  15. * @since 1.8
  16. */
  17. public long mappingCount() {
  18. long n = sumCount();
  19. return (n < 0L) ? 0L : n; // ignore transient negative values
  20. }
  21. final long sumCount() {
  22. CounterCell[] as = counterCells; CounterCell a;
  23. long sum = baseCount;
  24. if (as != null) {
  25. for (int i = 0; i < as.length; ++i) {
  26. if ((a = as[i]) != null)
  27. sum += a.value;//所有counter的值求和
  28. }
  29. }
  30. return sum;
  31. }

8.3 addCount方法

在put方法结尾处调用了addCount方法,把当前ConcurrentHashMap的元素个数+1这个方法一共做了两件事,更新baseCount的值,检测是否进行扩容。

  1. private final void addCount(long x, int check) {
  2. CounterCell[] as; long b, s;
  3. //利用CAS方法更新baseCount的值
  4. if ((as = counterCells) != null ||
  5. !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
  6. CounterCell a; long v; int m;
  7. boolean uncontended = true;
  8. if (as == null || (m = as.length - 1) < 0 ||
  9. (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
  10. !(uncontended =
  11. U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
  12. fullAddCount(x, uncontended);
  13. return;
  14. }
  15. if (check <= 1)
  16. return;
  17. s = sumCount();
  18. }
  19. //如果check值大于等于0 则需要检验是否需要进行扩容操作
  20. if (check >= 0) {
  21. Node<K,V>[] tab, nt; int n, sc;
  22. while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
  23. (n = tab.length) < MAXIMUM_CAPACITY) {
  24. int rs = resizeStamp(n);
  25. //
  26. if (sc < 0) {
  27. if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
  28. sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
  29. transferIndex <= 0)
  30. break;
  31. //如果已经有其他线程在执行扩容操作
  32. if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
  33. transfer(tab, nt);
  34. }
  35. //当前线程是唯一的或是第一个发起扩容的线程  此时nextTable=null
  36. else if (U.compareAndSwapInt(this, SIZECTL, sc,
  37. (rs << RESIZE_STAMP_SHIFT) + 2))
  38. transfer(tab, null);
  39. s = sumCount();
  40. }
  41. }
  42. }

ConcurrentHashMap源码分析(JDK8版本<转载>)的更多相关文章

  1. [JUC-5]ConcurrentHashMap源码分析JDK8

    在学习之前,最好先了解下如下知识: 1.ReentrantLock的实现和原理. 2.Synchronized的实现和原理. 3.硬件对并发支持的CAS操作及JVM中Unsafe对CAS的实现. 4. ...

  2. 并发-ConcurrentHashMap源码分析

    ConcurrentHashMap 参考: http://www.cnblogs.com/chengxiao/p/6842045.html https://my.oschina.net/hosee/b ...

  3. ConcurrentHashMap 源码分析

    ConcurrentHashMap 源码分析 1. 前言    终于到这个类了,其实在前面很过很多次这个类,因为这个类代码量比较大,并且涉及到并发的问题,还有一点就是这个代码有些真的晦涩,不好懂.前前 ...

  4. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

  5. Hashtable、ConcurrentHashMap源码分析

    Hashtable.ConcurrentHashMap源码分析 为什么把这两个数据结构对比分析呢,相信大家都明白.首先二者都是线程安全的,但是二者保证线程安全的方式却是不同的.废话不多说了,从源码的角 ...

  6. ConcurrentHashMap源码分析(一)

    本篇博客的目录: 前言 一:ConcurrentHashMap简介 二:ConcurrentHashMap的内部实现 三:总结 前言:HashMap很多人都熟悉吧,它是我们平时编程中高频率出现的一种集 ...

  7. ArrayList源码分析超详细(转载)

    ArrayList源码分析超详细   ArrayList源码分析超详解 想要分析下源码是件好事,但是如何去进行分析呢?以我的例子来说,我进行源码分析的过程如下几步: 找到类:利用 IDEA 找到所需要 ...

  8. ConcurrentHashMap源码分析(1.8)

    0.说明 1.ConcurrentHashMap跟HashMap,HashTable的对比 2.ConcurrentHashMap原理概览 3.ConcurrentHashMap几个重要概念 4.Co ...

  9. java基础系列之ConcurrentHashMap源码分析(基于jdk1.8)

    1.前提 在阅读这篇博客之前,希望你对HashMap已经是有所理解的,否则可以参考这篇博客: jdk1.8源码分析-hashMap:另外你对java的cas操作也是有一定了解的,因为在这个类中大量使用 ...

随机推荐

  1. Spring配置--Aop配置详情

    首先我们来看一下官方文档所给我们的关于AOP的一些概念性词语的解释: 切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象.事务管理是J2EE应用中一个关于横切关注点的很好的例子.在 ...

  2. stm32寄存器版学习笔记07 ADC

    STM32F103RCT有3个ADC,12位主逼近型模拟数字转换器,有18个通道,可测量16个外部和2个内部信号源.各通道的A/D转换可以单次.连续.扫描或间断模式执行. 1.通道选择 stm32把A ...

  3. __weak、__strong这样的关键词和weak、strong有哪些区别

    ios4 设备上最好就不要使用 ARC... strong,weak 用来修饰属性.strong 用来修饰强引用的属性:@property (strong) SomeClass * aObject;  ...

  4. [Aizu2784]Similarity of Subtrees

    vjudge Description 给一棵\(n\)个节点的有根树,定义两棵树同构当且仅当他们每个深度的节点个数相同.问这个树上有多少对子树满足同构.\(n\le100000\). sol 树\(h ...

  5. UVA11137 Ingenuous Cubrency

    题意 PDF 分析 考虑dp. 用\(d(i,j)\)表示用不超过i的立方凑成j的方案数. \(d(i,j)=d(i-1,j)+d(i,j-i^3)\) 时间复杂度\(O(IN+T)\) 代码 #in ...

  6. 一些IPC常用头文件

    //my_err.h#include <errno.h> /* for definition of errno */ #include <stdarg.h> /* ISO C ...

  7. linux多线程并发

    多线程并发 进程和线程的概念 进程 进程包括程序映象.地址空间等要素.内核采用PCB来管理进程.进程是内核进行调度的基本单元,每个独立的进程都有自己的代码段.数据段以及堆栈,它们有自己的虚拟地址空间, ...

  8. python文本挖掘模版

    import xlrd import jieba import sys import importlib import os #python内置的包,用于进行文件目录操作,我们将会用到os.listd ...

  9. 查询mysql数据库启动时间抛异常

    mysql 5.7.10使用dbforget Studio 连接异常 提示:The'INFORMATION_SCHEMA.SESSION_VARIABLES' feature is dis 查看mys ...

  10. python的可变数据类型和不可变类型

    python里面一切皆对象 ython的每个对象都分为可变类型和不可变类型 整形,浮点型,字符串,元组属于不可变类型,列表,字典是可变类型 不可变数据类型 对不可变类型的变量重新赋值,实际上是重新创建 ...