ConcurrentHashMap 究竟拿什么给你足够的安全感?

180 阅读15分钟

为什么要用CHM?

在多线程的情况下,hashmap够用吗?翻看源码,发现它的put方法中插入之前,都会对插入的位置进行散列位与计算,此时存在两个线程,一个线程判断位置为空,时间片用玩还未插入,另一个线程则也会在相同位置判断为空,于是他们的插入操作就会造成数据的覆盖。

//散列表下标与传入的key取得的散列值做位与计算,从而取得新的下标 
if ((p = tab[i = (n - 1) & hash]) == null) 
    tab[i] = newNode(hash, key, value, null);

于是JDK1.0就存在的,Hashtable就成为了候选,然而直接无脑用synchronized重量级锁锁方法,是否“过于”安全,就连简单的读取操作,互不影响,还不能同时进行实在是过于影响效率。

public synchronized V put(K key, V value) 
public synchronized V get(Object key)

JDK1.2提供的SynchronizedMap方法也是线程安全的,单也是"过于"安全的做法,直接在hashmap头上套一把对象锁,实在是锁向披靡。

Collections.synchronizedMap(new HashMap<>());

public V get(Object key) {
    synchronized (mutex) {return m.get(key);}
}

public V put(K key, V value) {
    synchronized (mutex) {return m.put(key, value);}
}

于是到了JDK1.5之后,Doug Lea为我们提供了全新的选择,它就是CHM(ConcurrentHashMap),而今比较经典的两个版本就是ConcurrentHashMap 1.7和1.8这两个版本:

JDK1.7 中 ConcurrentHashMap 采用的方案,被叫做 锁分段技术,每个部分就是一个 Segment(段)。但是,在JDK1.8中,完全重构了,采用的是 Synchronized + CAS ,把锁的粒度进一步降低,而放弃了 Segment 分段。(Synchronized升级后,采用锁升级也一定程度上提高了并发效率)

锁升级流程.png

CHM 1.7初探

在 JDK1.7中,本质上还是采用链表+数组的形式存储键值对的。原来的整个 table 划分为 n 个 Segment,每个 Segment 里边是由 HashEntry 组成的数组,HashEntry里又可以形成链表。

当对某个 Segment 加锁时,我们要做的就是尽可能的让元素均匀的分布在不同的 Segment中。最理想的状态是,所有执行的线程操作的元素都是不同的 Segment,这样就可以降低锁的竞争。

//Segment 对象,继承自 ReentrantLock 可重入锁。内部的属性和方法和 HashMap类似,只是多了一些拓展功能。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    //这是在 scanAndLockForPut 方法中用到的一个参数,用于计算最大重试次数
    //获取当前可用的处理器的数量,若大于1,则返回64,否则返回1。
    static final int MAX_SCAN_RETRIES =Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

    //用于表示每个Segment中的 table,是一个用HashEntry组成的数组。
    transient volatile HashEntry<K,V>[] table;

    //Segment中的元素个数,每个Segment单独计数(下边的几个参数同样的都是单独计数)
    transient int count;

    //每次 table 结构修改时,如put,remove等,此变量都会自增
    transient int modCount;

    //当前Segment扩容的阈值,同HashMap计算方法一样也是容量乘以加载因子
    //需要知道的是,每个Segment都是单独处理扩容的,互相之间不会产生影响
    transient int threshold;

    //加载因子
    final float loadFactor;
 }

// HashEntry,存在于每个Segment中,它就类似于HashMap中的Node,用于存储键值对的具体数据和维护单向链表的关系
static final class HashEntry<K,V> {
     //每个key通过哈希运算后的结果,用的是 Wang/Jenkins hash 的变种算法,此处不细讲,感兴趣的可自行查阅相关资料
     final int hash;
     final K key;
     //value和next都用 volatile 修饰,用于保证内存可见性和禁止指令重排序
     volatile V value;
     //指向下一个节点
     volatile HashEntry<K,V> next;
 }

CHM 1.8 源码解析

在 CHM 1.8 中,底层存储结构和 1.8 的 HashMap 是一样的,都是数组+链表+红黑树。引入了同步锁,在更细粒度的代码层面上,同步锁已经可以媲美 Lock 锁了。

image.png

sizeCtl的含义

private transient volatile int sizeCtl;

sizeCtl = 0, 代表数组未初始化, 且数组的初始容量为16

sizeCtl > 0,如果数组末初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值(数组的初始容量*0.75)

sizeCtl < 0,并且不是-1,表示数组正在扩容,-(1 +n) 表示此时有n个线程正在共同完成数组的扩容操作

sizeCtl = -1,表示数组正在进行初始化

必经之法

put方法

  • 新创建的CHM首次调用put方法,桶(数组)为空,会对CHM进行初始化,初始化是以CAS加自旋的方式进行的。

  • 桶(数组)不为空,则CHM初始化已完成,须先判断桶位的元素是否为空,如果为空才以CAS加自旋的方式进行元素的添加。如果桶(数组)属于扩容状态,则帮助桶扩容。

  • 否则该桶位的元素存在元素,先给该桶位加同步锁,之后给该桶位进行元素的添加,添加的形式可能以链表形式也可能以红黑树的形式。

  • 添加完成之后,先判断桶位节点数是否大于等于 8,如果大于等于 8则先判断桶的容量是否小于 64,如果小于64,则先进行扩容,直到达到树化最小容量64,再进行树化;否则直接树化。

public V put(K key, V value) {
    return putVal(key, value, false);
}
    
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //与hashmap不同,CHM的key值和value值都不能为空
    if (key == null || value == null) throw new NullPointerException();
    //spread方法与HashMap 的hash方法大同小异
    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();
        //取当前桶位下标元素赋值给f,并判空
        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
        }
        //如果该桶位元素hash值为MOVED,则表示该节点正在扩容,当前线程将会对它进行协助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //保证该桶位插入的操作线程安全
            synchronized (f) {
                //存在线程对该桶位树化或扩容导致该桶位元素改变,须校验
                if (tabAt(tab, i) == f) {
                    //fh >= 0则表示该桶位下是一个链表结构
                    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) {
                //如果节点个数大于等于 8,则转化为红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

spread 方法

0x7fffffff ,二进制为 0111 1111 1111 1111 1111 1111 1111 1111 。 hash值除了做了高低位异或运算,还多了一步,与 HASH_BITS 做与运算,保证最高位总是0,返回的hash值始终为正数。以便后续添加元素时判断该节点类型是链表还是红黑树。

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

initTable 方法

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            //sizeCtl<0,代表存在线程正在初始化或扩容,当前线程让出对CPU的使用而自旋
            Thread.yield(); // lost initialization race; just spin
        //利用Unsafe的CAS方法将sizeCtl赋值为-1,表示正在初始化
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                //double check 二次判空验证,防止其他线程已经初始化,当前线程重复初始化而使数据覆盖
                if ((tab = table) == null || tab.length == 0) {
                    //初始化数组的初始容量,传sizeCtl值则取其指定大小,否则取默认初始容量 16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //同 (1-1/4)n  -> 0.75n 
                    sc = n - (n >>> 2);
                }
            } finally {
                //将扩容阈值存入sizeCtl中
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

tabAt 方法

使用Unsaf.getObjectVolatile方法,是为了在不加锁的情况下保证数组中元素的一致性,数据从主内存读到线程的工作内存,保证线程间的可见性。

 private static final long ABASE;
 //table数组的第一个元素的起始地址
 private static final int ASHIFT;
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    //(long)i << ASHIFT) + ABASE 获取下标i元素的偏移地址
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

casTabAt 方法

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

treeifyBin 方法

private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

addCount 方法

put方法插入成功之后,会调用addCount方法,来统计桶位元素个数

如果在 HashMap 的 size 记数基础上,采用 volatile + CAS,也可以实现线程安全的记数,但高并发的状况下,同一时间操作size这个变量会造成严重的竞争。CHM进行了优化处理,将本应竞争的线程分散到CounterCell对象中,最后将CounterCell对象中的baseCount记数相加即可。

//线程被分配到的格子
@sun.misc.Contended static final class CounterCell {
 //此格子内记录的 value 值
    volatile long value;
    CounterCell(long x) { value = x; }
}

//用来存储线程和线程生成的随机数的对应关系
static final int getProbe() {
 return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

// x为1,check代表链表上的元素个数
private final void addCount(long x, int check) {
 CounterCell[] as; long b, s;
 //此处要进入if有两种情况
 //1.数组不为空,说明数组已经被创建好了。
 //2.若数组为空,说明数组还未创建,很有可能竞争的线程非常少,因此就直接 CAS 操作 baseCount
 //若 CAS 成功,则方法跳转到 (2)处,若失败,则需要考虑给当前线程分配一个格子(指CounterCell对象)
 if ((as = counterCells) != null ||
  !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
  CounterCell a; long v; int m;
  //字面意思,是无竞争,这里先标记为 true,表示还没有产生线程竞争
  boolean uncontended = true;
  //这里有三种情况,会进入 fullAddCount 方法
  //1.若数组为空,进方法 (1)
  //2.ThreadLocalRandom.getProbe() 方法会给当前线程生成一个随机数(可以简单的认为也是一个hash值)
  //然后用随机数与数组长度取模,计算它所在的格子。若当前线程所分配到的格子为空,进方法 (1)。
  //3.若数组不为空,且线程所在格子不为空,则尝试 CAS 修改此格子对应的 value 值加1。
  //若修改成功,则跳转到 (3),若失败,则把 uncontended 值设为 fasle,说明产生了竞争,然后进方法 (1)
  if (as == null || (m = as.length - 1) < 0 ||
   (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
   !(uncontended =
     U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
   //方法(1), 这个方法的目的是让当前线程一定把 1 加成功。情况更多,更复杂,稍后讲。
   fullAddCount(x, uncontended);
   return;
  }
  //(3)能走到这,说明数组不为空,且修改 baseCount失败,
  //且线程被分配到的格子不为空,且修改 value 成功。
  //但是这里没明白为什么小于等于1,就直接返回了,这里我怀疑之前的方法漏掉了binCount=0的情况。
  //而且此处若返回了,后边怎么判断扩容?(存疑)
  if (check <= 1)
   return;
  //计算总共的元素个数
  s = sumCount();
 }
 //(2)这里用于检查是否需要扩容(下边这部分很多逻辑不懂的话,等后边讲完扩容,再回来看就理解了)
 if (check >= 0) {
  Node<K,V>[] tab, nt; int n, sc;
  //若元素个数达到扩容阈值,且tab不为空,且tab数组长度小于最大容量
  while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
      (n = tab.length) < MAXIMUM_CAPACITY) {
   //这里假设数组长度n就为16,这个方法返回的是一个固定值,用于当做一个扩容的校验标识
   //可以跳转到最后,看详细计算过程,0000 0000 0000 0000 1000 0000 0001 1011
   int rs = resizeStamp(n);
   //若sc小于0,说明正在扩容
   if (sc < 0) {
       //sc的结构类似这样,1000 0000 0001 1011 0000 0000 0000 0001
    //sc的高16位是数据校验标识,低16位代表当前有几个线程正在帮助扩容,RESIZE_STAMP_SHIFT=16
    //因此判断校验标识是否相等,不相等则退出循环
    //sc == rs + 1,sc == rs + MAX_RESIZERS 这两个应该是用来判断扩容是否已经完成,但是计算方法存疑
    //感兴趣的可以看这个地址,应该是一个 bug ,
    // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427
    //nextTable=null 说明需要扩容的新数组还未创建完成
    //transferIndex这个参数小于等于0,说明已经不需要其它线程帮助扩容了,
    //但是并不说明已经扩容完成,因为有可能还有线程正在迁移元素。稍后扩容细讲就明白了。
    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
     sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
     transferIndex <= 0)
     break;
    //到这里说明当前线程可以帮助扩容,因此sc值加一,代表扩容的线程数加1
    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
     transfer(tab, nt);
   }
   //当sc大于0,说明sc代表扩容阈值,因此第一次扩容之前肯定走这个分支,用于初始化新表 nextTable
   //rs<<16
   //1000 0000 0001 1011 0000 0000 0000 0000
   //+2
   //1000 0000 0001 1011 0000 0000 0000 0010
   //这个值,转为十进制就是 -2145714174,用于标识,这是扩容时,初始化新表的状态,
   //扩容时,需要用到这个参数校验是否所有线程都全部帮助扩容完成。
   else if (U.compareAndSwapInt(this, SIZECTL, sc,
           (rs << RESIZE_STAMP_SHIFT) + 2))
    //扩容,第二个参数代表新表,传入null,则说明是第一次初始化新表(nextTable)
    transfer(tab, null);
   s = sumCount();
  }
 }
}

//扩容时的校验标识
static final int resizeStamp(int n) {
 return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

//Integer.numberOfLeadingZeros方法的作用是返回 n 的最高位为1的前面的0的个数
//n=16,
0000 0000 0000 0000 0000 0000 0001 0000
//前面有27个0,即27
0000 0000 0000 0000 0000 0000 0001 1011
//RESIZE_STAMP_BITS为16,然后 1<<(16-1),即 1<<15
0000 0000 0000 0000 1000 0000 0000 0000
//它们做或运算,得到 rs 的值
0000 0000 0000 0000 1000 0000 0001 1011

sumCount 方法

计算桶(数组)的元素总个数,即将baseCount和CounterCell数组中的value值累加,它们之和就是桶的元素总个数。

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    //baseCount,以这个值作为累加基准
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

helpTransfer方法

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
 Node<K,V>[] nextTab; int sc;
 //头结点为 ForwardingNode ,并且新数组已经初始化
 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;
   //当前线程需要帮助迁移,sc值加1
   if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
    transfer(tab, nextTab);
    break;
   }
  }
  return nextTab;
 }
 return table;
}

千金之法

fullAddCount 方法

addCount 方法中CounterCell数组不为空且CAS添加value失败后,进入到 fullAddCount 方法中。全力增加计数值,一定要成功。

  • 首先判断数组CounterCell[]是否为空,则先去创建数组;如果数组正在被创建且还未空,则直接对 baseCount进行CAS增加,成功则方法结束,失败则继续自旋直到baseCount增加成功或数组创建完成;

  • 如果数组不为空,可从以下逻辑图分析

fullAddCount逻辑流程图.png

//传过来的参数分别为 1 , false
private final void fullAddCount(long x, boolean wasUncontended) {
 int h;
 //如果当前线程的随机数为0,则强制初始化一个值
 if ((h = ThreadLocalRandom.getProbe()) == 0) {
  ThreadLocalRandom.localInit();      // force initialization
  h = ThreadLocalRandom.getProbe();
  //此时把 wasUncontended 设为true,认为无竞争
  wasUncontended = true;
 }
 //用来表示比 contend(竞争)更严重的碰撞,若为true,表示可能需要扩容,以减少碰撞冲突
 boolean collide = false;                // True if last slot nonempty
 //循环内,外层if判断分三种情况,内层判断又分为六种情况
 for (;;) {
  CounterCell[] as; CounterCell a; int n; long v;
  //1. 若counterCells数组不为空。  建议先看下边的2和3两种情况,再回头看这个。 
  if ((as = counterCells) != null && (n = as.length) > 0) {
   // (1) 若当前线程所在的格子(CounterCell对象)为空
   if ((a = as[(n - 1) & h]) == null) {
    if (cellsBusy == 0) {    
     //若无锁,则乐观的创建一个 CounterCell 对象。
     CounterCell r = new CounterCell(x); 
     //尝试加锁
     if (cellsBusy == 0 &&
      U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
      boolean created = false;
      //加锁成功后,再 recheck 一下数组是否不为空,且当前格子为空
      try {               
       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;
      }
      //若当前格子创建成功,且上边的赋值成功,则说明加1成功,退出循环
      if (created)
       break;
      //否则,继续下次循环
      continue;           // Slot is now non-empty
     }
    }
    //若cellsBusy=1,说明有其它线程抢锁成功。或者若抢锁的 CAS 操作失败,都会走到这里,
    //则当前线程需跳转到(9)重新生成随机数,进行下次循环判断。
    collide = false;
   }
   /**
   *后边这几种情况,都是数组和当前随机到的格子都不为空的情况。
   *且注意每种情况,若执行成功,且不break,continue,则都会执行(9),重新生成随机数,进入下次循环判断
   */
   // (2) 到这,说明当前方法在被调用之前已经 CAS 失败过一次,若不明白可回头看下 addCount 方法,
   //为了减少竞争,则跳转到⑨处重新生成随机数,并把 wasUncontended 设置为true ,认为下一次不会产生竞争
   else if (!wasUncontended)       // CAS already known to fail
    wasUncontended = true;      // Continue after rehash
   // (3) 若 wasUncontended 为 true 无竞争,则尝试一次 CAS。若成功,则结束循环,若失败则判断后边的 (4)(5)(6)。
   else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
    break;
   // (4) 结合 (6) 一起看,(4)(5)(6)都是 wasUncontended=true,且CAS修改value失败的情况。
   //若数组有变化,或者数组长度大于等于当前CPU的核心数,则把 collide 改为 false
   //因为数组若有变化,说明是由扩容引起的;长度超限,则说明已经无法扩容,只能认为无碰撞。
   //这里很有意思,认真思考一下,当扩容超限后,则会达到一个平衡,即 (4)(5) 反复执行,直到 (3) 中CAS成功,跳出循环。
   else if (counterCells != as || n >= NCPU)
    collide = false;            // At max size or stale
   // (5) 若数组无变化,且数组长度小于CPU核心数时,且 collide 为 false,就把它改为 true,说明下次循环可能需要扩容
   else if (!collide)
    collide = true;
   // (6) 若数组无变化,且数组长度小于CPU核心数时,且 collide 为 true,说明冲突比较严重,需要扩容了。
   else if (cellsBusy == 0 &&
      U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
    try {
     //recheck
     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;
    }
    //认为扩容后,下次不会产生冲突了,和(4)处逻辑照应
    collide = false;
    //当次扩容后,就不需要重新生成随机数了
    continue;                   // Retry with expanded table
   }
   // (9),重新生成一个随机数,进行下一次循环判断
   h = ThreadLocalRandom.advanceProbe(h);
  }
  //2.这里的 cellsBusy 参数非常有意思,是一个volatile的 int值,用来表示自旋锁的标志,
  //可以类比 AQS 中的 state 参数,用来控制锁之间的竞争,并且是独占模式。简化版的AQS。
  //cellsBusy 若为0,说明无锁,线程都可以抢锁,若为1,表示已经有线程拿到了锁,则其它线程不能抢锁。
  else if (cellsBusy == 0 && counterCells == as &&
     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
   boolean init = false;
   try {    
    //这里再重新检测下 counterCells 数组引用是否有变化
    if (counterCells == as) {
     //初始化一个长度为 2 的数组
     CounterCell[] rs = new CounterCell[2];
     //根据当前线程的随机数值,计算下标,只有两个结果 0 或 1,并初始化对象
     rs[h & 1] = new CounterCell(x);
     //更新数组引用
     counterCells = rs;
     //初始化成功的标志
     init = true;
    }
   } finally {
    //别忘了,需要手动解锁。
    cellsBusy = 0;
   }
   //若初始化成功,则说明当前加1的操作也已经完成了,则退出整个循环。
   if (init)
    break;
  }
  //3.到这,说明数组为空,且 2 抢锁失败,则尝试直接去修改 baseCount 的值,
  //若成功,也说明加1操作成功,则退出循环。
  else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
   break;                          // Fall back on using base
 }
}

transfer 方法

//这个类是一个标志,用来代表当前桶(数组中的某个下标位置)的元素已经全部迁移完成
static final class ForwardingNode<K,V> extends Node<K,V> {
 final Node<K,V>[] nextTable;
 ForwardingNode(Node<K,V>[] tab) {
  //把当前桶的头结点的 hash 值设置为 -1,表明已经迁移完成,
  //这个节点中并不存储有效的数据
  super(MOVED, null, null, null);
  this.nextTable = tab;
 }
}

//迁移数据
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
 int n = tab.length, stride;
 //根据当前CPU核心数,确定每次推进的步长,最小值为16.(为了方便我们以2为例)
 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
  stride = MIN_TRANSFER_STRIDE; // subdivide range
 //从 addCount 方法,只会有一个线程跳转到这里,初始化新数组
 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 指代新数组
  nextTable = nextTab;
  //这里就把推进的下标值初始化为原数组长度(以16为例)
  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
 //i 代表当前线程正在迁移的桶的下标,bound代表它本次可以迁移的范围下限
 for (int i = 0, bound = 0;;) {
  Node<K,V> f; int fh;
  //需要向前推进
  while (advance) {
   int nextIndex, nextBound;
   //(1) 先看 (3) 。i每次自减 1,直到 bound。若超过bound范围,或者finishing标志为true,则不用向前推进。
   //若未全部完成迁移,且 i 并未走到 bound,则跳转到 (7),处理当前桶的元素迁移。
   if (--i >= bound || finishing)
    advance = false;
   //(2) 每次执行,都会把 transferIndex 最新的值同步给 nextIndex
   //若 transferIndex小于等于0,则说明原数组中的每个桶位置,都有线程在处理迁移了,
   //于是,需要跳出while循环,并把 i设为 -1,以跳转到④判断在处理的线程是否已经全部完成。
   else if ((nextIndex = transferIndex) <= 0) {
    i = -1;
    advance = false;
   }
   //(3) 第一个线程会先走到这里,确定它的数据迁移范围。(2)处会更新 nextIndex为 transferIndex 的最新值
   //因此第一次 nextIndex=n=16,nextBound代表当次迁移的数据范围下限,减去步长即可,
   //所以,第一次时,nextIndex=16,nextBound=16-2=14。后续,每次都会间隔一个步长。
   else if (U.compareAndSwapInt
      (this, TRANSFERINDEX, nextIndex,
       nextBound = (nextIndex > stride ?
           nextIndex - stride : 0))) {
    //bound代表当次数据迁移下限
    bound = nextBound;
    //第一次的i为15,因为长度16的数组,最后一个元素的下标为15
    i = nextIndex - 1;
    //表明不需要向前推进,只有当把当前范围内的数据全部迁移完成后,才可以向前推进
    advance = false;
   }
  }
  //(4)
  if (i < 0 || i >= n || i + n >= nextn) {
   int sc;
   //若全部线程迁移完成
   if (finishing) {
    nextTable = null;
    //更新table为新表
    table = nextTab;
    //扩容阈值改为原来数组长度的 3/2 ,即新长度的 3/4,也就是新数组长度的0.75倍
    sizeCtl = (n << 1) - (n >>> 1);
    return;
   }
   //到这,说明当前线程已经完成了自己的所有迁移(无论参与了几次迁移),
   //则把 sc 减1,表明参与扩容的线程数减少 1。
   if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
    //在 addCount 方法最后,我们强调,迁移开始时,会设置 sc=(rs << RESIZE_STAMP_SHIFT) + 2
    //每当有一个线程参与迁移,sc 就会加 1,每当有一个线程完成迁移,sc 就会减 1。
    //因此,这里就是去校验当前 sc 是否和初始值是否相等。相等,则说明全部线程迁移完成。
    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
     return;
    //只有此处,才会把finishing 设置为true。
    finishing = advance = true;
    //这里非常有意思,会把 i 从 -1 修改为16,
    //目的就是,让 i 再从后向前扫描一遍数组,检查是否所有的桶都已被迁移完成,参看 (6)
    i = n; // recheck before commit
   }
  }
  //(5) 若i的位置元素为空,则说明当前桶的元素已经被迁移完成,就把头结点设置为fwd标志。
  else if ((f = tabAt(tab, i)) == null)
   advance = casTabAt(tab, i, null, fwd);
  //(6) 若当前桶的头结点是 ForwardingNode ,说明迁移完成,则向前推进 
  else if ((fh = f.hash) == MOVED)
   advance = true; // already processed
  //(7) 处理当前桶的数据迁移。
  else {
   synchronized (f) {  //给头结点加锁
    if (tabAt(tab, i) == f) {
     Node<K,V> ln, hn;
     //若hash值大于等于0,则说明是普通链表节点
     if (fh >= 0) {
      int runBit = fh & n;
      //这里是 1.7 的 CHM 的 rehash 方法和 1.8 HashMap的 resize 方法的结合体。
      //会分成两条链表,一条链表和原来的下标相同,另一条链表是原来的下标加数组长度的位置
      //然后找到 lastRun 节点,从它到尾结点整体迁移。
      //lastRun前边的节点则单个迁移,但是需要注意的是,这里是头插法。
      //另外还有一点和1.7不同,1.7 lastRun前边的节点是复制过去的,而这里是直接迁移的,没有复制操作。
      //所以,最后会有两条链表,一条链表从 lastRun到尾结点是正序的,而lastRun之前的元素是倒序的,
      //另外一条链表,从头结点开始就是倒叙的。看下图。
      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;
     }
    }
   }
  }
 }
}

相关资源

我就知道面试官接下来要问我 ConcurrentHashMap 底层原理了
JDK1.8并发容器ConcurrentHashMap - 掘金 (juejin.cn)
《吊打面试官》系列-ConcurrentHashMap & Hashtable
面试必备之ConcurrentHashMap终结篇_哔哩哔哩_bilibili