超详细JDK1.8版本的ConcurrentHashMap源码解析

提前预知

JDK1.7版本的HashMap结构

数组+链表,头插法!
在这里插入图片描述

  • HashMap多线程下是不安全的。

JDK1.7版本的ConcurrentHashMap结构

数组+链表, 头插法!
在这里插入图片描述

ConcurrentHashMap 能够实现线程安全且高效是因为采用了分段加锁的方式,其实就是把一个大ConcurrentHashMap分成了一段一段的 Segment,称之为段(SEGMENT)。

ConcurrentHashMap 的内部细分了若干个小的 HashMap,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 Segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

这么一理解 ConcurrentHashMap 与 HashTable 最大的区别就是ConcurrentHashMap 对大 table 中每个位置加锁,而 HashTable如果要加锁的话就是对整个 table 加锁,当然效率就高了。

图中ConcurrentHashMap 有 16 个 Segments,所以理论上,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上,16这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的,每个 Segment 内部更像是一个 HashMap,内部是支持扩容的。

再说说操作ConcurrentHashMap的几个常用的操作方法?

  • get() 方法:根据 key 找到对应的 Segment,再遍历 key 拿到具体的 HashEntry。

  • put() 方法:大致是先判断是否需要扩容,扩容整理后根据 key 找到对应的Segment,再往 Segment 中 put 键值对,这个时候 put 是加锁的,利用自旋锁去尝试获取锁,获取锁后判断 key 是否存在,存在就覆盖不存在就添加一个键值对。总之就是利用再入锁的方式锁住Segment,保证只有一个线程在操作 Segment,这就相当于在 HashMap 中保证了只有一个线程在数组的一个位置中 put,这当然不会形成环形链表了。

    ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽来说,在插入第一个值的时候进行初始化,延时初始化。

  • resize()方法 :该方法不需要考虑并发,因为到这里的时候,是持有该 Segment 的独占锁的。

  • get()方法:该操作是不加锁的。

JDK1.8版本的HashMap结构

数组+链表+红黑树,尾插法!
在这里插入图片描述

  • HashMap多线程下是不安全的。

JDK1.8版本的ConcurrentHashMap结构

数组+链表+红黑树,尾插法!
在这里插入图片描述
JDK1.8 版本的ConcurrentHashMap相比于JDK1.7版本的ConcurrentHashMap变化还是比较大的,首先取消了Segment,结构看起来和JDK1.8版本的HashMap差不多,只不多它是线程安全的。

线程安全是如何实现的那?

  • put() 的时候采用了 CAS + synchronized 保证线程安全
  • get() 就还是那样,读不影响线程安全,所以变化不大。

JDK1.7和JDK1.8版本的ConcurrentHashMap区别

  • JDK1.8 取消了 Segment 分段锁的数据结构,取而代之的是 数组+链表+红黑树 的结构。

  • JDK1.7采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock。JDK1.8采用 CAS+Synchronized 保证线程安全。

  • JDK1.7原来是对需要进行插入、修改、删除操作的 Segment(一个大TABLE分割成多个小 Segment) 加锁,现调整为对每个数组TABLE加锁(Node)。

  • 从原来的遍历链表 O(n),变成遍历红黑树 O(logN)。

  • JDK1.7链表采用头插法,JDK1.8链表采用尾插法。

1.8版本的ConcurrentHashMap底层数据结构

HashMap底层数据结构

在学习ConcurrentHashMap之前最好先学习HashMap

  • JDK1.7版本的HsashMap底层的数据结构是:数组+链表
  • JDK1.8版本的HsashMap底层的数据结构是:数组+链表+红黑树

为什么这样改那?

  • 为了效率,大家都知道这种结构存储速度大于数组查找速度大于链表,所以才会采用树这种数据结构。
  • 在JDK1.7中,如果链表过于长,就会出现查找效率比较低的情况,所以在JDK1.8中当链表过长就会将其转化为红黑树,为的就是查找的速度更快。
  • JDK1.8中,链表长度大于8的时候,就会变成红黑树,小于6的时候,就会有红黑树转化为链表

ConcurrentHashMap底层数据结构

ConcurrentHashMap底层数据结构其实是和HashMap的数据结构是一样的

  • JDK1.7版本的HsashMap底层的数据结构是:数组+链表
  • JDK1.8版本的HsashMap底层的数据结构是:数组+链表+红黑树

那为什么有了HashMap还要ConcurrentHashMap那?

  • 因为HashMap是线程不安全的,ConcurrentHashMap是线程安全的,在多线程高并发的情况下,如果要考虑数据的安全性,就要使用ConcurrentHashMap

1.8版本的ConcurrentHashMap中定义的属性

在分析源码之前我们先来看看ConcurrentHashMap中定义了那些属性

/**可以定义的最大容量*/
private static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* 默认的初始化容量,必须是2的幂
* 为什么必须是2的幂我们后面会详细介绍
*/
private static final int DEFAULT_CAPACITY = 16;

/**
* 可能的最大(非 2 的幂)数组大小
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
* 此表的默认并发级别
* 未使用,之所以定义是为了与之前的版本兼容。
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

/**
* 该表的加载因子。
* 在构造函数中覆盖此值仅影响初始表容量。 
*/
private static final float LOAD_FACTOR = 0.75f;

/**
* 链表树化的阈值
*/
static final int TREEIFY_THRESHOLD = 8;

/**
* 树化变链表的阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;

/**
* 当表的容量大于这个值也可能树化,表树化的最小容量.
* 否则,如果表中的节点过多,则调整表的大小。
* 该值应至少为 4 * TREEIFY_THRESHOLD 以避免调整大小和树化阈值之间发生冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64;

/**
* Minimum number of rebinnings per transfer step. Ranges are
* subdivided to allow multiple resizer threads.  This value
* serves as a lower bound to avoid resizers encountering
* excessive memory contention.  The value should be at least
* DEFAULT_CAPACITY.
*/
private static final int MIN_TRANSFER_STRIDE = 16;

/**
* The number of bits used for generation stamp in sizeCtl.
* Must be at least 6 for 32bit arrays.
*/
private static int RESIZE_STAMP_BITS = 16;

/**
* The maximum number of threads that can help resize.
* Must fit in 32 - RESIZE_STAMP_BITS bits.
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

/*
* Encodings for Node hash fields. See above for explanation.
*/
static final int MOVED     = -1; // hash for forwarding nodes
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

/** CPU 数量,多线程编发扩容时使用*/
static final int NCPU = Runtime.getRuntime().availableProcessors();

1.8版本的ConcurrentHashMap的构造方法

构造方法

我们先从构造方法下手,来慢慢的分析源码:

  • 无参构造:这个构造方法什么都没做,就是创建了一个ConcurrentHashMap的实例
public ConcurrentHashMap() {
    }
  • 带一个参数的构造方法:传入的参数是数组的大小
public ConcurrentHashMap(int initialCapacity) {
       //如果传入的初始化值小于0,直接抛出异常
       if (initialCapacity < 0)
           throw new IllegalArgumentException();
       /*
       * 如果这个值大于2的29次方,就赋值为MAXIMUM_CAPACITY
       * 如果小于这个值,就要调用tableSizeFor来进行计算
        * */
       int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                  MAXIMUM_CAPACITY :
                  tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
       // 把这个值赋给sizeCtl
       this.sizeCtl = cap;
   }
  • 带两个参数的构造方法:传入的值是数组大小和加载因子
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
		// 该方法调用了可以接受三个参数的构造方法
       this(initialCapacity, loadFactor, 1);
   }
  • 带三个参数的构造方法:传入的值是数组大小,加载因子,并发级别
public ConcurrentHashMap(int initialCapacity,
                            float loadFactor, int concurrencyLevel) {
       //判断是否都大于0
       if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
           throw new IllegalArgumentException();
       // 判断是否小于默认的并发级别,默认为16
       if (initialCapacity < concurrencyLevel)   // Use at least as many bins
           initialCapacity = concurrencyLevel;   // as estimated threads
       // 计算数组大小的size
       long size = (long)(1.0 + (long)initialCapacity / loadFactor);
       // 对传入放入size进行调整,获得真实的数组的大小
       int cap = (size >= (long)MAXIMUM_CAPACITY) ?
           MAXIMUM_CAPACITY : tableSizeFor((int)size);
       // 将这个大小赋值给sizeCtl变量
       this.sizeCtl = cap;
   }
  • 聪明的你会发现,我们创建完ConcurrentHashMap的实例之后,为什么没有开辟数组那,这就是牵涉到ConcurrentHashMap的延时创建了,当我们向里面put值的时候才会开辟数组,这里只是初始化了要开辟数组的大小sizeCtl
  • 后面会重点介绍这个put方法!!!!!

tableSizeFor()方法

在有参构造方法中都调用了这个方法,我们跟进看一下它是做什么的?

  • 我们跟进一下tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1))
/*
 * 这个方法就是保证不管你传入的是什么值,都会转化为比传入的值大的2的幂的数
 * 例如:假如你传入一个12,对应的二进制为 :1100  1100>>>1=0110为十进制6
 * tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1))
 * tableSizeFor(12 + (6) + 1))=tableSizeFor(17)
 *
 * int n=17-1;   16
 * n|=n>>>1;     0001 0000|0000 1000=0001 1000
 * n|=n>>>2;     0001 1000|0000 0110=0001 1110
 * n|=n>>>4;     0001 1100|0000 0001=0001 1111
 * n|=n>>>8;     0001 1111|0000 0000=0001 1111
 * n|=n>>>16;    0001 1111|0000 0000=0001 1111
 * (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1
 * 0001 1111+0000 0001=0010 0000=32 =>2的倍数
 * */
 private static final int tableSizeFor(int c) {
     int n = c - 1;
     n |= n >>> 1;
     n |= n >>> 2;
     n |= n >>> 4;
     n |= n >>> 8;
     n |= n >>> 16;
     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
 }

这个方法就是保证不管你传入的是什么值,都会转化为比传入的值大的2的幂的数,这也就是为什么ConcurrentHashMap的容量总是2的N次幂。

为什么必须是2的幂,其他值不行吗?

  • 这个问题的答案我们会在后面分析!!!!

1.8版本的ConcurrentHashMap的put方法

put方法

没什么好说的跟进看看putVal方法就行

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

putVal方法

代码很长,先大致理一下思路再看源码比较轻松:

  • 第一个判断:如果传入的key或者value有一个是null,就直接抛出异常
  • 进入for循环
    • CASE1:如果第一次put值,Node数组还没有初始化,则进入
    • CASE2:如果当前位置的值为null,则进入
    • CASE3:判断这个Node数组是不是在扩容,如果是,则进入
    • CASE4:前面条件执行完就代表有值且产生hash冲突,则进入
      • CASE4.1:向链表里面插
      • CASE4.2:向树里面插
final V putVal(K key, V value, boolean onlyIfAbsent) {
   //如果传入的key或者value有一个是null,就直接抛出异常
   if (key == null || value == null) throw new NullPointerException();

   //给传入的key计算hash值,然后在使用spread方法在扰动一下:为的就是尽可能的减少hash冲突
   int hash = spread(key.hashCode());

   //初始化一个整形变量并赋值0,用来记录链表的长度
   int binCount = 0;
   
   //这里其实就是自旋操作,因为没有结束条件
   for (Node<K,V>[] tab = table;;) {
       /**
       	* f:当前节点
       	* n:table数组长度
       	* i:数组索引
       	* fh:当前节点的hash值
       	*/
       Node<K,V> f; int n, i, fh;
       //如果tab为null并且tab的长度为0,说明还没有初始化Node数组
       if (tab == null || (n = tab.length) == 0)
           //调用initTable方法进行初始化,就是创建一个长度为sizeCtl大小的Node数组,并返回
           tab = initTable();
           
       /*
       * (f = tabAt(tab, i = (n - 1) & hash))等价于f=tab[i],可以先这个样理解,后面
       * 会说明这两中方式的不同
       * */
       else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
           // 通过CAS的方式进行添加,是线程安全的。
           // 如果cas失败,说明存在竞争,也就是判断完之后,有其他线程捷足先登了
           //这个时候方法会返回false,程序向下执行,找到存在hash冲突的方式再存值
           if (casTabAt(tab, i, null,
                        new Node<K,V>(hash, key, value, null)))
               break;                 
       }
       // 如果f=tab[i].hash的hash值为-1,表示这个map正在扩容
       else if ((fh = f.hash) == MOVED)
           //帮助去扩容,多线程扩容,该方法后面会详细介绍
           tab = helpTransfer(tab, f);
       //前面条件执行完就代表有值且产生hash冲突
       else {
           V oldVal = null;
           //为了并发安全,先把Node数组中的这个tab[i]元素锁住,也就是链表头结点或者树的根节点
           synchronized (f) {
               //加锁之后重新检查,防止加锁过程中被修改,tab[i]=f
               if (tabAt(tab, i) == f) {
                   //当前节点的hash值不小于0,表示是链表中的节点
                   if (fh >= 0) {
                       //计数加1,当大于8的时候就树化
                       binCount = 1;
                       //把f赋值给e,保护头结点,记录链表长度
                       for (Node<K,V> e = f;; ++binCount) {
                           //用来接收头结点的key
                           K ek;
                           //比较头结点的key和要插入的key的hash值,key是否相等
                           //如果上面判断成功,则进行值的替换
                           if (e.hash == hash &&
                               ((ek = e.key) == key ||
                                (ek != null && key.equals(ek)))) {
                               oldVal = e.val;
                               if (!onlyIfAbsent)
                                   e.val = value;
                               break;
                           }
                           //把头结点赋值给pred
                           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;
                       }
                   }
               }
           }
           //如果binCount不为0,就判断是否达到链表转换为红黑树的阈值
           //static final int TREEIFY_THRESHOLD = 8;
           /*
           * 为什么这个树化的方法不加锁?
           *   因为在树化的方法内部加了synchronized关键字,一样可以实现同步
           * */
           if (binCount != 0) {
               if (binCount >= TREEIFY_THRESHOLD)
                   treeifyBin(tab, i);
               if (oldVal != null)
                   return oldVal;
               break;
           }
       }
   }
   //统计一共put了多少entry到ConcurrentHashMap里面,看看是否达到扩容的阈值
   //此方法里面调用了真正的扩容方法,后面会好好分析!!!!
   addCount(1L, binCount);
   return null;
}
initTable()方法

如果ConcurrentHashMap是第一次put值,会进行初始化,调用这个函数,我们跟进看一看:

  • 使用sizeCtl初始化数组
    • CASE1: (sc = sizeCtl) < 0,礼让线程
    • CASE2:sc的值不为-1,说明没有其他线程进行初始化数组,就可以进入初始化这个数组。

private final Node<K,V>[] initTable() {
	/**
	 * tab:待初始化的table数组
	 * sc:数组的长度
	 */
   Node<K,V>[] tab; int sc;
   //把table赋值给tab,如果tab为null且tab长度为0,说明没有初始化,则进入循环体
   while ((tab = table) == null || tab.length == 0) {
       //来了!!!这个值sizeCtl在这里
       if ((sc = sizeCtl) < 0)
           //如果sc小于0,就礼让线程,开始自旋,出不来了,等待着sc大于等于0
           /**
           * 其实sc=sizrCtl,不同的值对应不同的操作
           *  -1时:表示有线程正在初始化,其他线程进来,就不需要再次初始化,等待其他线程初始化完成即可
           *  -N时:表示正在进行扩容操作,也需要礼让线程,一个线程扩容即可	
           *  正数:表示要创建数组的大小
           */
           Thread.yield();
       /*
       * 这里是一个CAS操作:线程安全的替换操作,其实相当于一个锁,当一个线程把sc这个值改为-1的
       * 时候,其它线程如果进来就只能因为CASE1条件进入自旋,等待sc这个值大于等于0
       * 
       * 这个SIZECTL是相对于this对象的在内存中的偏移量,指向的就是sizeCtl字段的值
       * 如果拿到的这个sc的值和sizeCtl字段的值相同,就将这个sizeCtl字段的值赋值为-1
       * 
       * 为什么前面sc=sizeCtl,这里会不相等那?
       *  这个就是多线程操作同一个ConcurrentHashMap的原因
       * */
       else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
       		// 初始化数组
           try {
               //把table赋值给tab,如果tab为null且tab长度为0,则进入
               if ((tab = table) == null || tab.length == 0) {
                   //如果sc大于0,n=sc
                   int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                   @SuppressWarnings("unchecked")
                   //创建一个长度为n的Node数组
                   Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                   // 把nt赋值给tab,tab赋值给table
                   table = tab = nt;
                   //把sc置为原来大小的0.75倍,扩容阈值
                   sc = n - (n >>> 2);
               }
           } finally {
               //将sc赋值给sizeCtl,为正数表示要扩容的阈值
               sizeCtl = sc;
           }
           break;
       }
   }
   //返回 tab
   return tab;
}
  • UnSafe.compareAndSwapInt()方法

在初始化的时候,里面调用了这个方法,这是一个本地方法:

/*
* 这里说一下compareAndSwapInt这个方法的参数
* var1:就是将要修改的值的对象
* var2:对象在内存中偏移量为offset处的值,结合object + offset能找到要修改的值的地址.
* var4:期望的值,就是拿这个值和 object + offset处存放的值进行比较;如果相同则修改,返回true,否则返回false,等下次修改.
* var5:如果上一步对比相等,则将这个值替换 object + offset地址处的值,然后返回true。
* */
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
tabAt()方法:

该方法内部调用了一个本地方法Unsafe.getObjectVolatile()

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);
  }
  • Unsafe.getObjectVolatile()方法:
/*
* 先说一下方法参数:
* var1:就是将要获取的对象
* var2:对象在内存中偏移量为offset处的值,结合var1 + var2能找到对应变量的地址.
*   
* 功能:该方法获取对象中offset偏移地址对应的对象field的值,
*  
* 为什么使用这种方式获取值?
* getObjectVolatile,一旦看到volatile关键字,就表示可见性
* 以volatile读的方式来读取table数组中的元素,保证每次拿到的数据都是最新的
* 虽然table数组本身是增加了volatile属性,但是volatile的数组只针对数组的引用具有volatile的语义,
* 而不是它的元素,所以如果有其他线程对这个数组的元素进行写操作,那么当前线程来读的时候不一定能读到最新的值
* */
public native Object getObjectVolatile(Object var1, long var2);
casTabAt()方法:

该方法内部调用了一个本地方法Unsafe.compareAndSwapObject()

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.compareAndSwapObject()方法:
/*
 * 先说一下方法参数:
 * var1 :包含要修改的字段对象;
 * var2 :字段在对象内的偏移量;
 * var4 : 字段的期望值;
 * var5 :如果该字段的值等于字段的期望值,用于更新字段的新值;
 * 功能:该方法找到offset偏移地址对应的对象field的值,如果和var4相等,就使用var5替换,不相等就自旋
 * */
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
helpTransfer()方法
  • 如果有线程正在扩容,就会调用helpTransfer方法帮助扩容,方法中会判断是否能够参与此次扩容,如果可以,就会CAS修改sizeCtl,将低16位的值+1,表示正在扩容的线程数加1。
/**
* 如果 resize 操作正在进行,帮助转移节点 f。
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
   Node<K,V>[] nextTab; int sc;
   // 如果 tab 不为 null,传进来的节点是 ForwardingNode,且 ForwardingNode
   // 的下一个 tab 不为 null
   if (tab != null && (f instanceof ForwardingNode) &&
           (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { //即nextTable有值,正在扩容。
       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;
           // CAS 更新帮助转移的线程数(+1)
           if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
               transfer(tab, nextTab);
               break;
           }
       }
       return nextTab;  //如果帮助扩容完成了,返回新的nextTab,
   }
   return table;//扩容完成,那么返回底层table
}

1.8版本的ConcurrentHashMap的扩容原理

addCount()方法

当向ConcurrentHashMap中put的元素达到了扩容的阈值,就会调用相应的方法进行扩容

putVal方法中调用了addCount(1L, binCount);方法来计算是否达到扩容阈值,我们来看一下:

这里面用到了并发统计相关的类LongAdder,具体的可以参考这篇博客:https://blog.csdn.net/weixin_45583303/article/details/119323778

/*
* 当数组中的容量大于扩容阈值时:sizeCtl,将进行扩容操作
* */
private final void addCount(long x, int check) {
	/**
	 * as:cells数组
	 * b:之前put进数组中总的个数,baseCount
	 * s:更新后的baseCount
	 */
   CounterCell[] as; long b, s;
   /**
    * 条件一:如果cells数组不为空,则进入
    * 条件二:CSA方式加一失败(存在线程竞争),则进入
    */ 
   if ((as = counterCells) != null ||
       !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
       /**
        * a:cell
        * v:cells数组对象位置的当前值
        * m:cells数组长度
        */
       CounterCell a; long v; int m;
       //竞争标志位,true表示没有竞争
       boolean uncontended = true;
       /**
        * 条件一:cells数组为空,则进入
        * 条件二:cells数组为空,主要是为了获取m值,则进入
        * 条件三:如果cells数组不为空,就获取当前线程对应的hash值,通过计算找到cells数组对应的
        * 		 位置,看看是否为null,为null,则进入
        * 条件四:条件三不为null,通过CAS对里面存的值进行加1操作,失败则进入(表示有竞争)
        * 		 uncontended =false
        * 
        * ThreadLocalRandom.getProbe() & m
        *   ThreadLocalRandom.getProbe():计算当前线程对应的hash值
        *   ThreadLocalRandom.getProbe() & m:类似于HashMap中查找桶位置一样,找到在cells数
        * 									  组中的位置
        * 注意:base值为CELLVALUE
        */
       if (as == null || (m = as.length - 1) < 0 ||
           (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
           !(uncontended =
             U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
           // 这个就是具体的对发生竞争时如何统计的代码了
           // 如果没有看过LongAdder的源码的同学,可以看了LongAdder的源码,再看这个方法的源码
           fullAddCount(x, uncontended);
           return;
       }
       // 如果check==binCount<=1,表示不存在多线程put,不需要进行统计操作,直接返回
       if (check <= 1)
           return;
       // 统计一下一共put了多个node到数组中
       s = sumCount();
   }
   /**
    * 前面统计完之后,这里就要判断是不是到达扩容阈值了
    */
   if (check >= 0) {
       Node<K,V>[] tab, nt; int n, sc;
       /**
        * 条件一:统计的值大于等于扩容阈值
        * 条件二:table数组不为null
        * 条件三:当前table数组长度小于最大值
        * 以上三个条件同时满足,则进入
        */
       while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
              (n = tab.length) < MAXIMUM_CAPACITY) {
           // 扩容标记。
           int rs = resizeStamp(n);
           /**
            * CASE1:sc<0,则进入
            * CSAE2:sc>=0,则进行CAS
            */
            //CASE1:sc<0,则进入
           if (sc < 0) {
           		/**
           		 * RESIZE_STAMP_BITS = 16;
           		 * SIZECT为sizeCtl字段的偏移量
           		 * MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1=(1<<16)-1
           		 * MAX_RESIZERS:最大可以帮助扩容的线程数
           		 * 
           		 * 条件一:sc无符号右移16为不等于rs,即sc的高16位不等于标识,则进入
           		 * 条件二:rs+1是等于sc,则进入
           		 * 条件三:rs+MAX_RESIZERS等于sc,则进入
           		 * 条件四: nextTable == null,说明当前扩容还没有初始化nextTable,则进入
           		 * 条件五:transferIndex <= 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);
           }
           //CSAE2:sc>=0,则行CAS
           // sc=rs左移16为+2
           else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                        (rs << RESIZE_STAMP_SHIFT) + 2))
                // 数据迁移操作
               transfer(tab, null);
           s = sumCount();
       }
   }
}
  • 这是对判断是否扩容时,while循环里面的条件分析
    在这里插入图片描述

  • resizeStamp(int n)方法:计算扩容标记的方法

/**
 * numberOfLeadingZeros(n):返回一个整数对应二进制数,高位补的0的个数
 * 例如:
 * 	1==>0000 0000 0000 0000 0000 0000 0000 0001==>32-1=31
 * 	2==>0000 0000 0000 0000 0000 0000 0000 0010==>32-2=30
 * 	3==>0000 0000 0000 0000 0000 0000 0000 0011==>32-2=30
 * private static int RESIZE_STAMP_BITS = 16; 
 * 	RESIZE_STAMP_BITS - 1=15
 *  1<< 15=0000 0000 0000 0000 1000 0000 0000 0000
 * 
 * 假如n=16
 *  16==>0000 0000 0000 0000 0000 0000 0001 0000==>32-5=27
 * 
 *       0000 0000 0000 0000 0000 0000 0001 1011   =  27     
 *  |
 *       0000 0000 0000 0000 1000 0000 0000 0000
 * 	=
 *       0000 0000 0000 0000 1000 0000 0001 1011=32795
 * 
 * 注意:这个扩容标记是一个只占用整型低十六位的数,map的容量越大(前面的0越少,求出来的值越少),
 * 或运算的结果也就越小
 */
static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

transfer()方法

真正的扩容方法是这个方法,addCount方法只是进行统计操作的,达到扩容阈值才会调用这个方法:

这个方法的代码比较长,我们先理一下思路:

  • 第一个开始扩容的线程创造一个容量为原容量两倍的新数组。

  • ConcurrentHashMap使用的是多线程扩容,每一个线程完成一段数组中节点的转移,用 stride 控制每个线程每一次需要处理的数组长度,用 transferIndex 记录已经处理过或者有线程正在处理的最小槽索引。

  • 自旋完成扩容操作,每一次自旋需要判断这一次处理的是哪一个位置,也就是 i 的位置,比 transferIndex 大的索引位置已经分配给之前的线程,当前线程从 transferIndex 位置开始处理,从后往前处理的索引范围是 transferIndex - stride 到 transferIndex

  • 处理完了当前位置 i 之后,继续处理 --i,如果当前范围内的所有位置都已经处理完了,根据 transferIndex 从 table 又分出一块 stride 给当前线程处理,这一流程是在 while (advance) {…} 这段代码中完成的。

  • 确定了应该处理哪一个位置之后,就可以执行转移操作了。

执行转移操作时主要有以下几种情况:

  • 如果当前线程已经完成转移,sizeCtl 减一后直接返回,最后一个线程完成扩容,设置 finishing 为 true 表示扩容结束,线程设置好 table、sizeCtl 变量之后,扩容结束。
  • 如果 i 位置节点为 null,将其设为 fwd,提醒其他线程该位已经处理过了。
  • 如果 i 位置已经处理过了,继续往后处理其他位置。该判断主要是最后i从n到0检查每一个桶是否转移完毕时用到。
  • 处理 i 位置。同样地,处理之前使用 synchronized 上锁。
  • 无论桶里是链式结构还是树状结构,都将链表拆分成两个链表,分别放在原位置和新位置上。具体实现上,使用了数组容量为 2 的幂这一点来简化操作(只判断标志位),使用了 lastRun 来提高效率。
/**
 * 移动和/或复制桶里的节点到新的 table 里。
 */
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // 确定步长,表示一个线程处理的数组长度,用来控制对 CPU 的使用,
    // 如果Cpu核数只有1,stride为n,如果不为1,则stride = tab.length/(NCPU*8),最小为 16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // 如果指定的 nextTab 为空(第一个线程开始扩容),初始化 nextTable
    // 其他线程进来帮忙时,不再创建新的 newTable。
    if (nextTab == null) {            // initiating
        try {
            // 创建一个相当于当前 table 两倍容量的数组,作为新的 table
            @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; //初始化完毕,nextTable就不为0,其他线程就可以帮忙转移了。
        transferIndex = n;    //0到transferIndex的位置是需要转移的桶所在的范围。
    }
    int nextn = nextTab.length; //新数组的长度,
    // fwd 是标志节点。当一个节点为空或者被转移之后,就设置为 fwd 节点
    // 表示这个桶已经处理过了
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // advance 标志指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
    boolean advance = true;
    // 在完成之前重新扫描一遍数组,确认已经完成。
    boolean finishing = false; // to ensure sweep before committing nextTab
    // 自旋移动每个节点,从 transferIndex 开始移动 stride 个槽的节点到新的
    // table
    // i 表示当前处理的节点索引,bound 表示需要处理节点的索引边界
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        //这个while在线程第一次进入,会进行CAS划分任务,如果可分配却CAS失败,
        // 就会再进while循环,直到得到任务,或者此时扩容任务已全部完成
        //在线程分配完任务后,会进第一个if条件,--i向前遍历,直到i到达bound,
        //到达后,如果当前transferIndex还是大于0,说明还有任务可以分配,
        //所以会进入到CAS中继续分配任务。
        //最后的结果就是
        //每个线程处理的区间为(nextBound, nextIndex)
        while (advance) {
            int nextIndex, nextBound;
            // 首先执行 i = i - 1,如果 i 大于 bound,说明还在当前 stride 范围内
            // nextIndex、nextBound、transferIndex 等都不需要改变
            // bound 是所有线程处理区间的最低点
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1; //-1 是为了进入后面的if判断,说明任务完成。
                advance = false;
            }
            // CAS更新 transferIndex,每一次transferIndex会减少一个stride,
            // 当前线程处理的桶区间为(nextBound, nextIndex)
            // 如果下一个开始往前遍历的起点是比stride大,说明可以进行一次划分任务,
            //如果小于等于stride,就说明不可划分了,当前线程的i初始会是-1.
            else if (U.compareAndSwapInt
                    (this, TRANSFERINDEX, nextIndex,
                            nextBound = (nextIndex > stride ?
                                    nextIndex - stride : 0))) {
                bound = nextBound; //bound为一次任务结束的边界,当i到达bound时,说明线程的任务完成了。
                i = nextIndex - 1;
                advance = false;
            }
        }
        //如果线程的i为-1,或者有出现扩容冲突,即可能进入到了协助扩容,
        // 但是扩容完成了,并且新的扩容开始了,将会导致i比原来的n要大,并且分配到任务但是在这里退出了。
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 已经完成转移,设置 table 为新的 table,更新 sizeCtl 为扩容后的
            // 0.75 倍(原容量的 1.5 倍)并返回
            if (finishing) {   //如果全部协助的线程都已经工作完毕,且sizeCtl和原来的值相等,设置了finnishing,说明扩容完成。
                nextTable = null;  //nextTable赋值为null,方便下次扩容。
                table = nextTab;   //底层table赋值为新表。
                sizeCtl = (n << 1) - (n >>> 1); //设置阈值。
                return;
            }
            // 当前线程 return 之后可能还有其他线程正在转移
            // 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
            //然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
            //这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // sc 初值为 (rs << RESIZE_STAMP_SHIFT) + 2)
                // 如果还有其他线程正在操作,直接返回,不改变 finishing,
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;

                // 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
                // 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
                finishing = advance = true;
                i = n; // recheck before commit,i会从n到0开始检查一遍。
            }
        }
        // 如果 i 位置节点为 null,那么放入刚刚初始化的 ForwardingNode ”空节点“,
        // 提醒其他线程该位已经处理过了
        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) {
                // 确认 i 位置仍然是 f,防止其他线程拿到锁进入修改
                if (tabAt(tab, i) == f) {
                    // ln 保留在原位置,hn 应该移到i + n 位置
                    Node<K,V> ln, hn;
                    // 如果当前为链表节点
                    if (fh >= 0) {
                        // n 为原 table 长度,且为 2 的幂,任何数与 n 进行 & 操作后
                        // 只可能是 0 或者 n。
                        // 根据这个把链表节点分成两类,为 0 说明原来的索引小于 n,
                        // 则位置保持不变,为 n 说明已经超过了原来的 n,新的位置
                        // 应该是 n + i(n 的某一位为 1,如果需要移动,该 bit 位也
                        // 必定为 1,不然将会待在原桶,位置不变)
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        //这个for循环,找到最后一个维持不变的lastRun,
                        //即lastRun后面的节点都是会分到同一个新表中的桶的。
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            // runBit 一直在变化
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        // 上面的循环执行完之后,lastRun 及其之后的元素在同一组。
                        // 且 runBit 就是 last 的标识
                        // 如果 runBit 等于 0,则 lastRun 及之后的元素都在原位置
                        // 否则,lastRun 及之后的元素都在新的位置
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        // 把 f 链表分成两个链表。
                        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);
                                // i + n 位置
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 上述循环完成转移之后桶内的顺序并不一定是原来的顺序了
                        // 原因是lastRun后面维持正常顺序,但是头插法会倒序。

                        // 在 nextTab 的 i 位置插入一个链表
                        setTabAt(nextTab, i, ln);
                        // nextTab 的 i + n 位置插入一个链表
                        setTabAt(nextTab, i + n, hn);
                        // table 的 i 位置插入 fwd 节点,表示已经处理过了
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }   //到这里处理完链表节点的一个桶了。
                    // 当前为树节点
                    else if (f instanceof TreeBin) {
                        // f 转为根节点
                        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) //赋值头节点,并设置p.prev
                                    lo = p;
                                else   //赋值普通节点的next。
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            // 应该放在 n + i 位置
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }//到这里红黑树已经分裂成2个双向链表,下一步要进行判断:
                        // 扩容后不再需要 tree 结构,转变为链表结构,需要就构建红黑树结构。
                        // 创建 TreeBin 时,其构造函数会把双向链表结构转化成树结构
                        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);  //在新表i位置上,放入新的节点。
                        setTabAt(nextTab, i + n, hn);  //在新表i+n上。放入新节点
                        setTabAt(tab, i, fwd);    //旧表的相应位置。设置为fwd,说明该节点已经处理完毕。
                        advance = true;    //当前节点处理完毕,提示要进行下一个节点处理。
                    }
                }
            }
        }
    }
}
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

彤彤的小跟班

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

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

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

打赏作者

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

抵扣说明:

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

余额充值