图解 ConCurrentHashMap ——从源码层面,弄清楚它是怎么控制并发的

在上上篇文章《HashMap 与 ConCurrentHashMap的简单原理》中,

笼统介绍了,这两个Map 共同的数据结构。

在上篇《HashMap 源码解析》,详细解析了HashMap 的源码。

本篇分析 ConCurrentHashMap 的源码,侧重讲解与 HashMap 不同的地方。

如果前两篇文章不熟悉,出门左拐,先看那两篇。

本文源代码取 java 1.8 版本。

先提醒下,本文分析的超级详细,文章特别的长!!

一、添加元素


  public V put(K key, V value) {
      return putVal(key, value, false);
  }

  /** Implementation for put and putIfAbsent */
  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);
          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;
  }

if (key == null || value == null) throw new NullPointerException();

这行说明它与HashMap 的一点不同。

ConCurrentHashMap key 和 value 都不可以是null,而 HashMap 则无此限制。


  int hash = spread(key.hashCode());

  static final int spread(int h) {
      return (h ^ (h >>> 16)) & HASH_BITS;
  }
  static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
  

这段是它的 哈希函数,也就是求数组下标的,解析 HashMap 源码时讲过。

不明白可以看《hash & (n - 1)》。

HASH_BITS = 0x7fffffff; 这个数字,转化为二进制,是31个1

和它进行与运算,那也那结果一定大于0。这个很重要!!

正常结点的 hash 大于 0 。

  • 初始化
  if (tab == null || (n = tab.length) == 0)
      tab = initTable();

这段是数组为空,初始化数组,相当于上节讲的 resize() 方法,等会再详细说。

  • 目标位置为空,直接设置

   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
   }

(n - 1) & hash 这个是哈希函数,用来算下标,上篇讲过。


   static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
       return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
   }

   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);
   }

这两个方法,是 直接操作 Unsafe 类,

tabAt 是返回数组指定下标的元素,

casTabAt 是 CAS 方式,在指定下标处设值。

这里再讲的仔细点 ((long)i << ASHIFT) + ABASE 这个算出来是什么?

    Class<?> ak = Node[].class;
    ABASE = U.arrayBaseOffset(ak); // 起始位置
    int scale = U.arrayIndexScale(ak); // 一个元素的大小(int 4字节,long 8 字节)
    if ((scale & (scale - 1)) != 0)
        throw new Error("data type scale not a power of two");
    ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

    ((long)i << ASHIFT) + ABASE  // 相当于数组的寻址公式

在《为什么数组下标从0开始》,这篇文章中, 说过,

数组的寻址公式是 a[i]_address = base_address + i*data_type_size


  public static void main(String[] args) throws Exception {
      Field f = Unsafe.class.getDeclaredField("theUnsafe");
      f.setAccessible(true);
      Unsafe U = (Unsafe) f.get(null);
      Class<String[]> ak = String[].class;
      int base = U.arrayBaseOffset(ak);
      log.info("base:{}", base); // 16,即起始是16
      int scale = U.arrayIndexScale(ak);
      log.info("scale:{}", scale); // 4,即偏移量是 4
      int shift = 31 - Integer.numberOfLeadingZeros(scale);
      log.info("shift:{}",shift); // 2 
      for(int i = 0; i < 5; i++){
          long result = ((long) i << shift) + base; // i 扩大4倍,加上 base
          log.info("result:{}",result);
      }
  }
    

我写了个demo,来模拟这个过程, String 类型的数组,

((long) i << shift) + base; 在本例中就是 i << 2 + 16

寻址公式,应该是 16 + i * 4 这俩一个效果。

base 为什么是16?

数组对象,对象头8字节、指针4字节、数组长度 4字节。所以从16开始。
在这里插入图片描述
其实 new 一个数组对象出来,内存会开辟一块连续的空间,

前面是对象头、指针、记录长度,最后才是数据。

啰啰嗦嗦讲这么多,(Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE)

这个本质是寻址公式,也就是 tab[i]。

HashMap 用的是 tab[i],简单明了,明明白白。

ConCurrentHashMap 用的 native Object getObjectVolatile(Object var1, long var2);

效果都是查看数组某下标处的元素,后者更多是从并发角度来考虑的。

transient volatile Node<K,V>[] table; 虽然用了 volatile,线程间可见,

网上说,数组是线程间可见,但数组元素未必。

ConCurrentHashMap 从并发角度考虑,用了更为底层的方法来查看元素。
.

  • 插入元素遇到扩容
  else if ((fh = f.hash) == MOVED)
      tab = helpTransfer(tab, f);
      
  static final int MOVED     = -1; // hash for forwarding nodes

这里先记住,当 hash 值是 -1时,说明正在扩容。

也就是说,插入元素时,正好在扩容,就调用 helpTransfer(tab, f); 一起扩容

即A线程触发了扩容,此时B线程插入元素,

那么B线程和A线程一起来完成扩容。

开始我看这段的时候,也懵,B线程来插入元素的,跑去扩容,那还插入不?

当然B还是要插入的,为什么?

  for (Node<K,V>[] tab = table;;) { 
	……
  }

翻上去看下,这是个无限循环。

B参与扩容之后,会再循环,最终肯定会执行它的插入操作。

helpTransfer(tab, f); 这个帮助扩容的方法,等会再细讲。
.

  • 存在哈希冲突
   else {
       V oldVal = null;
       synchronized (f) {
       		……
       }
       if (binCount != 0) {
           if (binCount >= TREEIFY_THRESHOLD)
               treeifyBin(tab, i);
           if (oldVal != null)
               return oldVal;
           break;
       }
   }

遇到哈希冲突时,代码的逻辑与 Hashmap 的差不多,要么按链表处理,要么按红黑树处理。

不同的是有 synchronized 关键字,即加锁处理。

f 是什么?前面说了 f = tabAt(tab, i = (n - 1) & hash) 是数组中该下标的元素。

在这里插入图片描述
在《HashMap 与 ConCurrentHashMap基本原理》中,说过其加锁的事儿,

这个粒度很细,对数组某下标元素加锁,不影响数组的其它位置。

即兼顾效率,又保证安全性。Doug Lea 真牛。

addCount(1L, binCount); 这行代码类似是扩容,等会儿再细讲。

至此,put() 方法大逻辑讲完了,与 HashMap 极其相似。

其中并发作了充分的控制,总结下有以下几点

  1. 初始化会并发控制
  2. 扩容会并发控制
  3. 查看数组某下标元素,使用 Unsafe 类中的 native 方法
  4. 扩容遇到并发,协助扩容
  5. 哈希冲突时,对相应数组下标元素加锁

二、数组初始化

上面说过,put 方法招行时,若数组未初始化,会调用 initTable() 方法


 if (tab == null || (n = tab.length) == 0)
     tab = initTable();


  private final Node<K,V>[] initTable() {
      Node<K,V>[] tab; int sc;
      while ((tab = table) == null || tab.length == 0) {
          if ((sc = sizeCtl) < 0)
              Thread.yield(); // lost initialization race; just spin
          else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
              try {
                  if ((tab = table) == null || tab.length == 0) {
                      int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                      @SuppressWarnings("unchecked")
                      Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                      table = tab = nt;
                      sc = n - (n >>> 2);
                  }
              } finally {
                  sizeCtl = sc;
              }
              break;
          }
      }
      return tab;
  }

这里有一个全局变量,是用来标识初始化的

    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
    private transient volatile int sizeCtl;

如果 sizeCtl = -1,说明是在初始化,如果 -(n+1) 说明有 n 个线程在扩容。

ConcurrentHashMap 初始化时,会设置sizeCtl

  public ConcurrentHashMap(int initialCapacity,
                           float loadFactor, int concurrencyLevel) {
      if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
          throw new IllegalArgumentException();
      if (initialCapacity < concurrencyLevel)   // Use at least as many bins
          initialCapacity = concurrencyLevel;   // as estimated threads
      long size = (long)(1.0 + (long)initialCapacity / loadFactor);
      int cap = (size >= (long)MAXIMUM_CAPACITY) ?
          MAXIMUM_CAPACITY : tableSizeFor((int)size);
      this.sizeCtl = cap;
  }

看过上篇《HashMap源码分析》,这段代码应该能看懂, sizeCtl 是 2 的 n 次方

明白了这些,下面这段就不用解释了

   if ((sc = sizeCtl) < 0)
       Thread.yield(); // lost initialization race; just spin

接着看下句 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1))

这是用 CAS 方法,将参数 sizeCtl 设置为 -1,若不成功,则进入下一次的循环。

  try {
      if ((tab = table) == null || tab.length == 0) {
          int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
          @SuppressWarnings("unchecked")
          Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
          table = tab = nt;
          sc = n - (n >>> 2);
      }
  } finally {
      sizeCtl = sc;
  }
  break;
     

这段就很好理解了,sc 是 2 的 n 次方,new 一个以此为大小的数组(大小为n)。

之后 sc 设置为 n 的 3/4,最后 sc 赋值给参数 sizeCtl

也就是说,数组初始化后,容量一定是 2 的 n 次方, sizeCtl 是容量的 3/4

此时,sizeCtl 是扩容的阈值,等会儿会讲到。

三、触发扩容

在 put 方法最后一段 addCount(1L, binCount); 是可能触发扩容 。

另外、链表转红黑树时,若数组容量小于64,也会触发扩容。(hashMap 讲过)

网上讲扩容的文章,大把。可讲触发扩容,很少。

基本上一句话带过,或压根不讲,可能因为这段比不好讲吧。


  private final void addCount(long x, int check) {
      CounterCell[] as; long b, s;
      if ((as = counterCells) != null ||
          !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
          CounterCell a; long v; int m;
          boolean uncontended = true;
          if (as == null || (m = as.length - 1) < 0 ||
              (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
              !(uncontended =
                U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
              fullAddCount(x, uncontended);
              return;
          }
          if (check <= 1)
              return;
          s = sumCount();
      }
      if (check >= 0) {
          Node<K,V>[] tab, nt; int n, sc;
          while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                 (n = tab.length) < MAXIMUM_CAPACITY) {
              int rs = resizeStamp(n);
              if (sc < 0) {
                  if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                      sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                      transferIndex <= 0)
                      break;
                  if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                      transfer(tab, nt);
              }
              else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                           (rs << RESIZE_STAMP_SHIFT) + 2))
                  transfer(tab, null);
              s = sumCount();
          }
      }
  }

这段代码不多,可极其复杂,只讲 put 方法调用时,它是怎么触发扩容的。

咱只说第一个触发扩容的那个线程。其它的后面再说。

put 方法在最后调用 addCount(1L, binCount);


  private final void addCount(long x, int check) {
      CounterCell[] as; long b, s;
      if ((as = counterCells) != null ||
          !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
          ……
      }
      if (check >= 0) {
          Node<K,V>[] tab, nt; int n, sc;
          while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                 (n = tab.length) < MAXIMUM_CAPACITY) {
              int rs = resizeStamp(n);
              if (sc < 0) {
                  ……
              }
              else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                           (rs << RESIZE_STAMP_SHIFT) + 2))
                  transfer(tab, null);
              s = sumCount();
          }
      }
  }

s = b + x 这个是当前存入 key-value的总数。这个先记住,后面再解释。

if (check >= 0) 这个肯定满足。

while (s >= (long)(sc = sizeCtl) 这个条件是,达到扩容的阈值了。

前面说过,数组初始化之后,sizeCtl 是扩容的阈值。

if (sc < 0) {} 这个先不考虑,直接看下面,触发扩容。


 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);

这段的意思就是,将 参数 sizeCtl 设置为某个值,设置成功就执行 transfer(tab, null);

简单的说,就是在某种情况下,启动扩容。

sizeCtl 在执行数组初始化时,会设置为 -1

初始化完成时,会设置为扩容阈值

扩容时,会是负数,并会记录有几个线程在扩容。

下面咱们看下,扩容时, sizeCtl 是怎么设置的。

rs << RESIZE_STAMP_SHIFT) + 2,这个值为负,低16位是 0000 0000 0000 0010

我摘录几段代码,看了就清楚了。

//第一条扩容线程设置的某个特定基数
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)

//后续线程加入扩容大军时每次加 1
U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)

//线程扩容完毕退出扩容操作时每次减 1
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)

第一行代码先别问为什么,记住,等会说。

这三行代码,很清楚 sc 值增加或减少,都是CAS 操作。


	int rs = resizeStamp(n);  // n 是数组长度,

    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

	U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)

    private static int RESIZE_STAMP_BITS = 16;
    

resizeStamp(n) 是计算 rs 的。

Integer.numberOfLeadingZeros(n) 这个方法指,简单说,就是这个二进制数前面几个 0。

比如 2 的二进制是 10,前面应该是 30 个0,那这个方法就返回 30。

4 的二进制是 100,前面应该是 29 个 0,那这个方法返回 29。

1 << (RESIZE_STAMP_BITS - 1) 这个相当于 1 左移 15 位。高16位是0,拼 1 个1,再拼 15 个0。

我以n = 16 为例,画了图,图好解释。
在这里插入图片描述
可以很清楚的看到,在扩容时,sizeCtl 这个参数,一定是负数

高16位,保存了扩容前数组的容量信息,

低16位,保存了扩容的线程数量,(低16位若是 n,则 n -1 即为扩容的线程数)

为什么不从1开始计数呢?

因为数组初始化时,sizeCtl 设置为 -1,那个位置被占了。

至此,触发扩容最基本的流程,讲了一遍。下面讲 addCount(1L, binCount); 方法中这段


    if (sc < 0) {
        if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
            transferIndex <= 0)
            break;
        if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
            transfer(tab, nt);
    }

当一个参与扩容的线程,干完自己的活儿。一看,还需要扩容,就会执行这段。

while 循环中,会重新计算 sc 的值,小于0,就是扩容还在进行。

会调用 U.compareAndSwapInt(this, SIZECTL, sc, sc + 1),调用成功,就参与扩容。

直到扩容完成,或是扩容线程数达到了最大值,跳出循环结束。

非竞争状态下,addCount() 方法就讲到这儿,竞争状态下,先不讲,后面再讲。

四、扩容


  private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
      int n = tab.length, stride;
      if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
          stride = MIN_TRANSFER_STRIDE; // subdivide range
      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 = nextTab;
          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
      for (int i = 0, bound = 0;;) {
          Node<K,V> f; int fh;
          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))) {
                  bound = nextBound;
                  i = nextIndex - 1;
                  advance = false;
              }
          }
          if (i < 0 || i >= n || i + n >= nextn) {
              int sc;
              if (finishing) {
                  nextTable = null;
                  table = nextTab;
                  sizeCtl = (n << 1) - (n >>> 1);
                  return;
              }
              if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                  if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                      return;
                  finishing = advance = true;
                  i = n; // recheck before commit
              }
          }
          else if ((f = tabAt(tab, i)) == null)
              advance = casTabAt(tab, i, null, fwd);
          else if ((fh = f.hash) == MOVED)
              advance = true; // already processed
          else {
              synchronized (f) {
                  if (tabAt(tab, i) == f) {
                      Node<K,V> ln, hn;
                      if (fh >= 0) {
                          int runBit = fh & n;
                          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;
                      }
                  }
              }
          }
      }
  }

代码很长,也很不好理解。先不解析源码,先要明白以下几点。

原数组长度 n,新数组长度为 2n。扩容时,每次转移固定的节点数(stride)。

在这里插入图片描述

比如上图,当前线程,转移红色的节点。

若出现并发,并发线程转移灰色的节点。

即从末尾开始转移,每次固定的步长(长度 stride);

  1. 当本次transfer()结束后,调用方会决定是否再次调用transfer()
  2. 并发扩容时,每个线程,转移各自的节点(长度 stride)。
  3. 多个 stride,从旧数组末尾开始算,不重不漏。
  4. CAS操作 transferIndex,用于并发状态下,控制不同线程的。
  5. 具体节点从旧数组,转移到新数组,代码与HashMap相似。

好,看过这几条之后,开始一点一点解析源码。

   if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
       stride = MIN_TRANSFER_STRIDE; // subdivide range

   private static final int MIN_TRANSFER_STRIDE = 16;

这个是算步长的,和CPU数有关。步长最小值是 16。

   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 = nextTab;
       transferIndex = n;
   }

这段和上面讲过的,启动扩容的源码,transfer(tab, null); 传入的新数组是 null。

在创建新数组时,会设置 transferIndex = n

    /**
     * The next table index (plus one) to split while resizing.
     */
    private transient volatile int transferIndex;

当出现并发扩容时,这个全局变量,是用来给各个线程分配节点的。


    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true; // 是否结束 while 循环
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) { }
      

这里 for 循环做了两件事,先是计算需要转移的节点,再是将每个节点的数据进行转移


   while (advance) {
       int nextIndex, nextBound;
       if (--i >= bound || finishing)
           ……
       else if ((nextIndex = transferIndex) <= 0) {
           ……
       }
       else if (U.compareAndSwapInt
                (this, TRANSFERINDEX, nextIndex,
                 nextBound = (nextIndex > stride ?
                              nextIndex - stride : 0))) {
           bound = nextBound;
           i = nextIndex - 1;
           advance = false;
       }
   }
   

这里的三个分支,最后一个是计算需要转移的节点。

nextBound = (nextIndex > stride ? nextIndex - stride : 0) 这行不难理解。

比如原数组长度是 64,那新数组长度是 128。transferIndex 的初始值是 64

stride 是 16,那计算出 bound = nextBound = 48, i = 63

U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound )

执行之后,transferIndex 被 CAS 操作为 48。

那本次转移的节点,就是下标 48 到下标题 63 之间的数据。

如果并发扩容,别的线程进来了,会以新的transferIndex = 48 来计算,需要转移哪些数据。

以上分析了并发扩容时,对不同线程处理节点的控制。

Doug Lea 真的是牛,不用锁,仅用 CAS 控制一个全局变量,

就很巧妙的控制了并发扩容问题。

前面说过,for 循环做了两件事,先是计算本次需要转移的节点

再是将每个节点的数据进行转移。现在讲数据迁移的问题。

接着上面的例子,计算出 i = 63


    if (i < 0 || i >= n || i + n >= nextn) { // 结束扩容的,先不用管
        ……
    }
    else if ((f = tabAt(tab, i)) == null)
        advance = casTabAt(tab, i, null, fwd);
    else if ((fh = f.hash) == MOVED)
        advance = true; // already processed
    else {
        synchronized (f) {
        ……
        setTabAt(tab, i, fwd); // 将旧数组该节点处 hash 设置为 MOVED
        advance = true;
        }
    }

f = tabAt(tab, i) 这是取出该节点,判断这个节点,有三种情况,

  1. 节点为 null (即没有数据要迁移)
  2. hash 值为 -1,会再下一次for循环中,开启while循环
  3. 其它情况 进行数据迁移,将旧数组设置一个特殊节点 fwd

假设 i = 63 这个节点是 null,属于第一种情况,

那就执行 advance = casTabAt(tab, i, null, fwd);

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

    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
     }

fwd 是一个空节点,只是 hash 值是 MOVED( 即-1 )。

执行 advance = casTabAt(tab, i, null, fwd); ,这是CAS 操作,

如果失败,结束本次 for 循环,下次 for 循环还会执行这行,

如果成功,advance = true,结束本次 for 循环,下次 for 循环时,

开启 while 循环, while (advance) {}


   while (advance) {
       int nextIndex, nextBound;
       if (--i >= bound || finishing)
           advance = false;
       else if ((nextIndex = transferIndex) <= 0) {
           ……
       }
       else if ( ) {
          ……
       }
   }
   

进入 while 循环后,执行 --i >= bound 这行,i 从 63 变为 62。相当于处理下个节点。

也就是说,在上个循环中,处理 i = 63 时,会有三种情况,现在完善下,就是

  1. 节点为 null ,设置一个 MOVED 节点,结束本次循环,在下次循环时,下标会减1。
  2. 节点的 hash 值为 MOVED,说明本节点已处理过,结束本次循环,在下次循环时,下标减1。
  3. 其它情况 进行数据迁移,即头节点加锁,迁移数据,完成迁移后,设置hash 值为 MOVED,结束本次循环,下次循环时,下标减1。

总之会将节点设置为 MOVED,并将下标减小 1 。

那什么时候,结束数据迁移呢?

还是以上面的例子,转移下标 48 到 63 的数据。

当这几个节点都转移完成了。即 i = 48 会再次进入 while 循环

bound = nextBound = 48 这两个参数,最开始计算过,是 48。

  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))) {
          bound = nextBound;
          i = nextIndex - 1;
          advance = false;
      }
  }

if (--i >= bound || finishing) 这个不满足,

else if ((nextIndex = transferIndex) <= 0) 这个也不满足,

那就进入第三个分支,重新计算下标,设置 transferIndex 这个全局变量。

在这里插入图片描述
就比如说,某线程是转移 红色的那几个节点,

转移完了,灰色的几个节点分给其它线程了,

那这个线程再分配时,可能就分配到绿色的那几个节点。

当所有节点都分配完了,那就该结束 transfer 这个方法了。


  if (i < 0 || i >= n || i + n >= nextn) {
      int sc;
      if (finishing) {
          nextTable = null;
          table = nextTab;
          sizeCtl = (n << 1) - (n >>> 1);
          return;
      }
      if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
          if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
              return;
          finishing = advance = true;
          i = n; // recheck before commit
      }
  }
       

整体代码的思路是:

多个线程在扩容,当某线程完成自己的扩容任务时,退出

最后一个扩容线程,将 finishing 设置为 true。

然后将旧数组检查一遍(即再次走一遍扩容代码),

确实是所有节点都已完成迁移,设置相关参数,结束扩容。

U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1) 这行之前说过,

某线程完成分给它的迁移任务,退出时,记录扩容 线程的数量要减小1。

if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) 这行是不是有点懵。

在讲 addCount() 触发扩容时,讲了这段


   else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)){
        transfer(tab, null);
   }

触发扩容的那个线程,设置 sizeCtl 的值是 rs << RESIZE_STAMP_SHIFT) + 2

之后若有线程加入扩容,sizeCtl 的值 加 1,有线程退出扩容,这个值 减 1。

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

它的意思是,当前扩容线程数量不是1。

如果当前扩容线程数量是 1,

那应该是 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT

当前扩容线程数量是 1 时,会设置 finishing = advance = true;

这会开启 while 循环。另外设置 i = n;

这会从旧数组最后一个节点开始,重新执行扩容代码,

检查所有的节点 hash 值是否等于 MOVED。

检查完毕,执行下面的代码


  if (finishing) {
      nextTable = null;  // 参数置空
      table = nextTab; // table 使用新数组
      sizeCtl = (n << 1) - (n >>> 1); // sizeCtl 设置为当前数组大小的 0.75 倍,做为下次扩容的阈值。
      return;
  }

头节点加锁,进行数据迁移,这个与 HashMap 的代码雷同。

这里不再分析,不清楚的可以参考《HashMap 源码解析》。

至此,整个扩容的源码流程分析完了。

五、协助扩容

在讲 put() 方法时,说过,当插入元素时,遇到扩容,会协助扩容。

协助扩容的方法是 helpTransfer(tab, f);

  else if ((fh = f.hash) == MOVED)
      tab = helpTransfer(tab, f);

  final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
      Node<K,V>[] nextTab; int sc;
      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;
              if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                  transfer(tab, nextTab);
                  break;
              }
          }
          return nextTab;
      }
      return table;
  }

整个代码并不复杂,其中并发控制的非常好。

   if (tab != null && (f instanceof ForwardingNode) &&
       (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
       ……
   }

这个 if 判断,比较好理解 f instanceof ForwardingNode 正在扩容的意思。

int rs = resizeStamp(tab.length); 这个前面讲过,也画过图,是记录扩容状态的。


	int rs = resizeStamp(tab.length);

    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

在这里插入图片描述


   while (nextTab == nextTable && table == tab &&
          (sc = sizeCtl) < 0) {
       // 要么扩容结束了,跳出循环
       // 要么扩容没结束,成功调用了扩容方法,跳出循环
       // 扩容没结束,也没有调用扩容方法,进入下一次循环
   }

(sc = sizeCtl) < 0 这个条件说明,正在扩容,前面讲过。

nextTab == nextTable && table == tab 个人认为,这个防止 扩容里套用扩容。


  if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
      sc == rs + MAX_RESIZERS || transferIndex <= 0)
      break;
      

这个意思是扩容结束了,跳出while 循环。

sc >>> RESIZE_STAMP_SHIFT) != rs 这个看下上面的图,

sc 的高 16 位,与 rs 的低 16 位,应该是相等的

不相等,那就是标识符被改动了。

sc == rs + 1 左边是负数,右边是正数,这个永远是 flase, JDK 8 的 bug

sc == rs + MAX_RESIZERS 这也是bug,同上

它是想说明 扩容线程达到最大值了。

sizeCtl 的 低 16 位记录扩容的线程数,当低16位占满了。

说明有6万多个线程在扩容,再也不能多了。

transferIndex <= 0 这个也好解释,参与扩容的线程都会分若干个结点扩容。

拿上面的例子来讲,第一个触发扩容的线程,

先转移 下标 48 - 63 的节点,此时 transferIndex 从 64 变为 48,

再来一个线程,那就转移 下标为 32 - 47 的节点,此时 transferIndex 从48 变为 32

transferIndex <= 0 那就是所有节点都分出去了,不需要再协助扩容了。


   if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
       transfer(tab, nextTab);
       break;
   }
     

这段就容易理解,扩容前,将 sizeCtl 加 1,表示多了一个线程参与扩容,

执行完 transfer(),要么是扩容线程数减 1,

要么是扩容完全结束了,sizeCtl 新数组长度的 0.75倍,作为下次扩容的阈值。

六、查找元素

  public V get(Object key) {
      Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
      int h = spread(key.hashCode()); 
      if ((tab = table) != null && (n = tab.length) > 0 &&
          (e = tabAt(tab, (n - 1) & h)) != null) {
          if ((eh = e.hash) == h) { 
              if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                  return e.val;
          }
          else if (eh < 0)
              return (p = e.find(h, key)) != null ? p.val : null;
          while ((e = e.next) != null) {
              if (e.hash == h &&
                  ((ek = e.key) == key || (ek != null && key.equals(ek))))
                  return e.val;
          }
      }
      return null;
  }

(e = tabAt(tab, (n - 1) & h)) != null 这句,是定位到某个下标,该下标处不为 null 。

这里顺便总结一个:

  1. 正常节点 hash 值 大于等于 0
  2. hash 值 等于 -1,说明正在进行扩容
  3. hash 值 等于 -2,说明是一个红黑树节点
  if ((eh = e.hash) == h) { 
      if ((ek = e.key) == key || (ek != null && key.equals(ek)))
          return e.val;
  }

这段很好理解,头节点 hash 值符合,若 key 那就说明找到了,直接返回。

   else if (eh < 0)
       return (p = e.find(h, key)) != null ? p.val : null;

  /**
   * Virtualized support for map.get(); overridden in subclasses.
   */
  Node<K,V> find(int h, Object k) {
      Node<K,V> e = this;
      if (k != null) {
          do {
              K ek;
              if (e.hash == h &&
                  ((ek = e.key) == k || (ek != null && k.equals(ek))))
                  return e;
          } while ((e = e.next) != null);
      }
      return null;
  }

这段代码虽然很短,但是有没有疑问呢?

前面我说的很清楚、扩容与红黑树的节点,都会是 hash < 0。

难道这两种情况,走的同样的代码?

不是这样的,注释写的很清楚 overridden in subclasses.

当hash = -1,时,节点就是 ForwardingNode,这个内部类会重写 find 方法。

当 hash = -2 时,节点就是 TreeBin,这个内部类也会重写 find 方法。


  static final class ForwardingNode<K,V> extends Node<K,V> {
      final Node<K,V>[] nextTable;
      ForwardingNode(Node<K,V>[] tab) {
          super(MOVED, null, null, null);
          this.nextTable = tab;
      }

      Node<K,V> find(int h, Object k) {
          // loop to avoid arbitrarily deep recursion on forwarding nodes
          outer: for (Node<K,V>[] tab = nextTable;;) {
              Node<K,V> e; int n;
              if (k == null || tab == null || (n = tab.length) == 0 ||
                  (e = tabAt(tab, (n - 1) & h)) == null)
                  return null;
              for (;;) {
                  int eh; K ek;
                  if ((eh = e.hash) == h &&
                      ((ek = e.key) == k || (ek != null && k.equals(ek))))
                      return e;
                  if (eh < 0) {
                      if (e instanceof ForwardingNode) {
                          tab = ((ForwardingNode<K,V>)e).nextTable;
                          continue outer;
                      }
                      else
                          return e.find(h, k);
                  }
                  if ((e = e.next) == null)
                      return null;
              }
          }
      }
  }

这个代码不难理解,只是我水平有限,有些地方还是没想清楚,不细说了。

红黑树的代码我不会,同样也不讲。

加过头,继续说 get 方法的最后一段

  while ((e = e.next) != null) {
      if (e.hash == h &&
          ((ek = e.key) == key || (ek != null && key.equals(ek))))
          return e.val;
  }

走到这里,说明是一个正常状态下的链表,遍历这个链表即可。

纵观整个 get 方法,没有用锁,也没有CAS。会不会 脏读呢?


  static class Node<K,V> implements Map.Entry<K,V> {
      final int hash;
      final K key;
      volatile V val;
      volatile Node<K,V> next;
      ……
   }

首先,Node 类中,相关属性是volatile 修饰,能保证线程的可见性。

另外, transient volatile Node<K,V>[] table; 能保证扩容时,设置了新数组,

其它线程不会去读旧值。

也就是说, get 方法通过 volatile 避免了脏读。
.

七、删除元素

  public V remove(Object key) {
      return replaceNode(key, null, null);
  }


  final V replaceNode(Object key, V value, Object cv) {
      int hash = spread(key.hashCode());
      for (Node<K,V>[] tab = table;;) {
          Node<K,V> f; int n, i, fh;
          if (tab == null || (n = tab.length) == 0 ||
              (f = tabAt(tab, i = (n - 1) & hash)) == null)
              break;
          else if ((fh = f.hash) == MOVED)
              tab = helpTransfer(tab, f);
          else {
              V oldVal = null;
              boolean validated = false;
              synchronized (f) {
                  if (tabAt(tab, i) == f) {
                      if (fh >= 0) {
                          validated = true;
                          for (Node<K,V> e = f, pred = null;;) {
                              K ek;
                              if (e.hash == hash &&
                                  ((ek = e.key) == key ||
                                   (ek != null && key.equals(ek)))) {
                                  V ev = e.val;
                                  if (cv == null || cv == ev ||
                                      (ev != null && cv.equals(ev))) {
                                      oldVal = ev;
                                      if (value != null)
                                          e.val = value;
                                      else if (pred != null)
                                          pred.next = e.next;
                                      else
                                          setTabAt(tab, i, e.next);
                                  }
                                  break;
                              }
                              pred = e;
                              if ((e = e.next) == null)
                                  break;
                          }
                      }
                      else if (f instanceof TreeBin) {
                          validated = true;
                          TreeBin<K,V> t = (TreeBin<K,V>)f;
                          TreeNode<K,V> r, p;
                          if ((r = t.root) != null &&
                              (p = r.findTreeNode(hash, key, null)) != null) {
                              V pv = p.val;
                              if (cv == null || cv == pv ||
                                  (pv != null && cv.equals(pv))) {
                                  oldVal = pv;
                                  if (value != null)
                                      p.val = value;
                                  else if (t.removeTreeNode(p))
                                      setTabAt(tab, i, untreeify(t.first));
                              }
                          }
                      }
                  }
              }
              if (validated) {
                  if (oldVal != null) {
                      if (value == null)
                          addCount(-1L, -1);
                      return oldVal;
                  }
                  break;
              }
          }
      }
      return null;
  }

虽然方法很长,其实逻辑还是很简单的。

  for (Node<K,V>[] tab = table;;) {
      Node<K,V> f; int n, i, fh;
      if (tab == null || (n = tab.length) == 0 ||
          (f = tabAt(tab, i = (n - 1) & hash)) == null)
          break;
      else if ((fh = f.hash) == MOVED)
          tab = helpTransfer(tab, f);
      else {
          // 删除元素
          synchronized (f){...}
      }
  }
  return null;

总体是 for 循环里三个分支,

第一个分支 :数组不存在,或该下标处为空,跳出 循环返回 null 。

第二个分支:删除元素时,遇到 扩容。那参与扩容,完成后本次循环结束,进入下一次循环。

第三个分支:加锁删除元素,hash > 0,说明是链表节点,hash < 0,说明是红黑树节点。

只说链表节点删除,红黑树的我不会哈。


 for (Node<K,V> e = f, pred = null;;) {
      K ek;
      if (e.hash == hash &&
          ((ek = e.key) == key ||
           (ek != null && key.equals(ek)))) {
          V ev = e.val;
          if (cv == null || cv == ev ||
              (ev != null && cv.equals(ev))) {
              oldVal = ev;
              if (value != null)
                  e.val = value;
              else if (pred != null)
                  pred.next = e.next;
              else
                  setTabAt(tab, i, e.next);
          }
          break;
      }
      pred = e;
      if ((e = e.next) == null)
          break;
  }

大概逻辑就是,遍历该下标处所有元素,找到对应的 key,返回 其 value,删除此节点。

  for (Node<K,V> e = f, pred = null;;) {
       K ek;
       if () {
       //  找到对应的key
          break;
       }
       pred = e;
       if ((e = e.next) == null)
           break;
   }

这是一个循环,会将此下标处的元素遍历一遍。

e 指当前节点, pred 指 e 的父亲节点。

e.next 不等于 null,会遍历下该节点的下一个节点。

如果找到,两种处理方式,当前节点是头节点,当前节点存在父节点。

分别做下面的处理。


 else if (pred != null)
     pred.next = e.next;
 else
     setTabAt(tab, i, e.next);
	     

最终,oldValue 不是 null,会修改键值对的数量。

    if (oldVal != null) {
        if (value == null)
            addCount(-1L, -1);
        return oldVal;
    }

需要说明一点,在同一下标处,删除元素与插入元素,是存在竞争的。

锁的是同一个对象,可认为是竞争同一把锁。不能同时进行。

但不同下标处,删除与插入,彼此无影响,锁资源不同。

ConCurrentHashMap 锁粒度是很细的。

在保证安全的前提下,尽可能的提高了性能。

八、计数

所谓的计数,指的是 ConCurrentHashMap 存了多少个 键值对。


   public int size() {
       long n = sumCount();
       return ((n < 0L) ? 0 :
               (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
               (int)n);
   }

比如调用 size 方法,返回键值对的个数。这里是把 long 型,强转为 int。

当然还有更为精确的方法,mappingCount


   public long mappingCount() {
       long n = sumCount();
       return (n < 0L) ? 0L : n; // ignore transient negative values
   }

这两个方法,都是调用了 sumCount() 方法。


   final long sumCount() {
       CounterCell[] as = counterCells; CounterCell a;
       long sum = baseCount;
       if (as != null) {
           for (int i = 0; i < as.length; ++i) {
               if ((a = as[i]) != null)
                   sum += a.value;
           }
       }
       return sum;
   }

代码很清晰,比较容易理解,计数来自两部分,一个是 baseCount

另一个是 各个 CounterCell 的和。

  /**
   * Base counter value, used mainly when there is no contention,
   * but also as a fallback during table initialization
   * races. Updated via CAS.
   */
  private transient volatile long baseCount;
  
  /**
   * Table of counter cells. When non-null, size is a power of 2.
   */
  private transient volatile CounterCell[] counterCells;

  /**
   * A padded cell for distributing counts.  Adapted from LongAdder
   * and Striped64.  See their internal docs for explanation.
   */
  @sun.misc.Contended static final class CounterCell {
      volatile long value;
      CounterCell(long x) { value = x; }
  }

源码中注释写的很清楚,counterCells 大小是 2 的 n 次方。

CounterCell 这个内部类中的 成员变量只有一个,且是用 vllatile 修饰的。

@sun.misc.Contended 这个可以解决**伪共享**的问题,本文不再展开讲。

以上代码很容易理解,计数方法也没有要讲的。

这里,重点说说,counterCells 是如何初始化的,它的工作原理

以及在多线程环境下,是如何计数的。

重点说明:

先明确一点,计数时,要么修改了 baseCount,要么 修改了 CounterCell 对象中 value的值

前面讲触发扩容的时候,说了 addCount(1L, binCount); 这个方法。

另外在讲删除元素 remove 方法时,也调用了 addCount(-1L, -1); 这个方法。

这个方法其中一个重要功能,就是计数。


 private final void addCount(long x, int check) {
     CounterCell[] as; long b, s;
     if ((as = counterCells) != null ||
         !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
         CounterCell a; long v; int m;
         boolean uncontended = true;
         if (as == null || (m = as.length - 1) < 0 ||
             (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
             !(uncontended =
               U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
             fullAddCount(x, uncontended);
             return;
         }
         if (check <= 1)
             return;
         s = sumCount();
     }
     if (check >= 0) {
		// 可能触发扩容,前面讲过了,跟计数无关系,这里省略
     }
 }

假设 counterCells 还没有初始化,现在有 4 个线程,同时执行为个方法,

   if ((as = counterCells) != null ||
       !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) 

(as = counterCells) != null,按假设来说,这个返回 false,看下一个判断。

!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)

四个线程同时执行这行,那只有一个线程会执行成功,

baseCount 修改,不进入方法体。

其它三个线程执行方法体中的方法。

    if (as == null || (m = as.length - 1) < 0 ||
        (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
        !(uncontended =
          U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
        fullAddCount(x, uncontended);
        return;
    }

按假设来讲,as == null 返回 true,三个线程都会执行 fullAddCount(x, uncontended);

这个方法就是进行精确的计数的。等会再细讲。

现在假设 counterCells 已经初始化,且 size 大于0。

还是 4 个线程同时执行 addCount 方法。

   if ((as = counterCells) != null ||
       !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) 

(as = counterCells) != null,按假设来说,这个返回 true,

后半个判断就不会执行了,四个线程都进入方法体。

四个线程都会执行 (a = as[ThreadLocalRandom.getProbe() & m]) == null

ThreadLocalRandom.getProbe() 这个方法是返回一个随机数,彼此之间不同。

as[ThreadLocalRandom.getProbe() & m] 这个是得到数组中的一个元素。

求下标的公式和 HashMap 的一样,这里也不展开来说。

每个线程获取的随机数是不一样的,各自算出一个下标。

假设极端情况下,4 个线程计算出的下标是同一个。

若该下标处元素为 null,那 4 个线程,都会执行 fullAddCount(x, uncontended);

若该下标处元素不为null,那 4 个线程都执行

!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))

那有一个线程成功修改 counterCell 中 value 的值,完成这个线程的计数,返回。

剩下的3个线程,都会执行 fullAddCount(x, uncontended);

总结下:

执行 addCount 时计数时,

counterCells 这个数组未初始化, 非竞争条件下,修改 baseCount,否则执行 fullAddCount(x, uncontended);

counterCells 这个数组已经初始化, 非竞争条件下,修改 对应的 counterCell,否则执行 fullAddCount(x, uncontended);

如果上面的都清楚了,咱就开始分析 fullAddCount(x, uncontended); 方法

 private final void fullAddCount(long x, boolean wasUncontended) {
     int h;
     if ((h = ThreadLocalRandom.getProbe()) == 0) {
         ThreadLocalRandom.localInit();      // force initialization
         h = ThreadLocalRandom.getProbe();
         wasUncontended = true;
     }
     boolean collide = false;                // True if last slot nonempty
     for (;;) {
         CounterCell[] as; CounterCell a; int n; long v;
         if ((as = counterCells) != null && (n = as.length) > 0) {
             if ((a = as[(n - 1) & h]) == null) {
                 if (cellsBusy == 0) {            // Try to attach new Cell
                     CounterCell r = new CounterCell(x); // Optimistic create
                     if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                         boolean created = false;
                         try {               // Recheck under lock
                             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;
                         }
                         if (created)
                             break;
                         continue;           // Slot is now non-empty
                     }
                 }
                 collide = false;
             }
             else if (!wasUncontended)       // CAS already known to fail
                 wasUncontended = true;      // Continue after rehash
             else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                 break;
             else if (counterCells != as || n >= NCPU)
                 collide = false;            // At max size or stale
             else if (!collide)
                 collide = true;
             else if (cellsBusy == 0 &&
                      U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                 try {
                     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;
                 }
                 collide = false;
                 continue;                   // Retry with expanded table
             }
             h = ThreadLocalRandom.advanceProbe(h);
         }
         else if (cellsBusy == 0 && counterCells == as &&
                  U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
             boolean init = false;
             try {                           // Initialize table
                 if (counterCells == as) {
                     CounterCell[] rs = new CounterCell[2];
                     rs[h & 1] = new CounterCell(x);
                     counterCells = rs;
                     init = true;
                 }
             } finally {
                 cellsBusy = 0;
             }
             if (init)
                 break;
         }
         else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
             break;                          // Fall back on using base
     }
 }

代码看起来很长,但分解开来,没有特别难的。


  if ((h = ThreadLocalRandom.getProbe()) == 0) {
      ThreadLocalRandom.localInit();      // force initialization
      h = ThreadLocalRandom.getProbe();
      wasUncontended = true;
  }
    

这段是为了让线程获取随机数,先让其初始化。

具体细节就不展开讲了,有兴趣的,可以看下 ThreadLocalRandom 源码。


 for (;;) {
     CounterCell[] as; CounterCell a; int n; long v;
     if ((as = counterCells) != null && (n = as.length) > 0) {
         // 修改某个CounterCell,有可能会对 counterCells 进行扩容
     }
     else if (cellsBusy == 0 && counterCells == as &&
              U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
		// 初始化 counterCells
     }
     else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
         break;                          // Fall back on using base
         // 修改 baseCount 的值
 }

这整体是一个无限循环,三个分支执行成功一个,就可以跳出循环。

否则一直执行。最终的结果是 要么修改了**baseCount** ,要么 修改了 CounterCell

先看第二个分支,初始化 counterCells

   CounterCell[] as; CounterCell a; int n; long v;
  if ((as = counterCells) != null && (n = as.length) > 0){
  ……
  }
  else if (cellsBusy == 0 && counterCells == as &&
           U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
      boolean init = false;
      try {                           // Initialize table
          if (counterCells == as) {
              CounterCell[] rs = new CounterCell[2];
              rs[h & 1] = new CounterCell(x);
              counterCells = rs;
              init = true;
          }
      } finally {
          cellsBusy = 0;
      }
      if (init)
          break;
  }

cellsBusy == 0 && counterCells == as 这两个判断是并发的控制。

U.compareAndSwapInt(this, CELLSBUSY, 0, 1) 这个是CAS修改 cellsBusy

cellsBusy 是 1 指在初始化或是在操作 CounterCells

执行成功的那个线程,初始化**counterCells**


    /**
     * Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
     */
    private transient volatile int cellsBusy;
    

然后看方法体


  boolean init = false;
  try {                           // Initialize table
      if (counterCells == as) {
          CounterCell[] rs = new CounterCell[2];
          rs[h & 1] = new CounterCell(x);
          counterCells = rs;
          init = true;
      }
  } finally {
      cellsBusy = 0;
  }
  if (init)
      break;
      

**CounterCell**的初始容量是2,并且把 new 一个 CounterCell 对象,记录了 数值

初始化完毕后,将 cellsBusy 设置为 0;

再看第一个分支


 CounterCell[] as; CounterCell a; int n; long v;
 if ((as = counterCells) != null && (n = as.length) > 0) {
     if ((a = as[(n - 1) & h]) == null) {
         if (cellsBusy == 0) {            // 当前没有线程操作数组
             CounterCell r = new CounterCell(x); // new 一个对象出来
             if (cellsBusy == 0 &&
                 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { // CAS 设置 cellsBusy 为1
                 boolean created = false; // 开关
                 try {               // Recheck under lock
                     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; // 操作完后,一定将 cellsBusy 改回 0 
                 }
                 if (created)
                     break; // 设置到成功的情况下,退出。
                 continue;           // Slot is now non-empty
             }
         }
         collide = false;
     }
     else if (!wasUncontended)       // CAS already known to fail
         wasUncontended = true;      // Continue after rehash
     else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
         break; // 对应下标处不为空,且CAS 记录数据成功,退出
     else if (counterCells != as || n >= NCPU) // 当数组size 大于CPU 数量,不让数组扩容。
         collide = false;            // At max size or stale
     else if (!collide)
         collide = true;
     else if (cellsBusy == 0 &&
              U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { 
         try { // 执行扩容
             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;
         }
         collide = false;
         continue;                   // Retry with expanded table
     }
     h = ThreadLocalRandom.advanceProbe(h);
 }

if ((a = as[(n - 1) & h]) == null) 意思是 某下标处元素为空,这个小分支,

总的逻辑就是,线程安全情况下, new 一个 CounterCell 对象,记录了数值,设置到数组中。

这个应该好理解,我把注释写代码里了。

第二个大分支就是想办法设置 CounterCell,必要情况下扩容数组。

第三个大分支更好理解,修改 baseCount


 else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
     break; 

fullAddCount 方法 代码虽然很长,但各分支逻辑很清晰。

就是:在充分控制并发的情况下,修改 CounterCell 或是 baseCount。

修改不成功,就重试,直到成功为止。

九、总结

本篇文章,在HashMap的基础上,分析了 ConcurrentHashMap 是如何控制并发的。

  • 触发扩容、扩容、协助扩容、计数等等操作,大量用了CAS 操作,
  • 在具体删除、添加节点操作,用了 synchronized 锁,
  • 扩容时,专门有 ForwardingNode 对象,transferIndex、sizeCtl属性来控制

源码很复杂,也很精妙,本人水平有限,只分析了部分常用的方法。

有分析不到位或是错误的地方,烦请批评指正。

若是对解析的某些方法,有疑问,欢迎在留言区讨论。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值