ConcurrentHashMap源码解析,多线程扩容 (2)

因此我们可以先看一下ConcurrentHashMap 里面的比较重要的参数:

//最大容量 private static final int MAXIMUM_CAPACITY = 1 << 30; //默认初始化容量 private static final int DEFAULT_CAPACITY = 16; //负载因子 private static final float LOAD_FACTOR = 0.75f; //链表转为红黑树的临界值 static final int TREEIFY_THRESHOLD = 8; //红黑树转为链表的临界值 static final int UNTREEIFY_THRESHOLD = 6; //当容量大于64时,链表才会转为红黑树,否则,即便链表长度大于8,也不会转,而是会扩容 static final int MIN_TREEIFY_CAPACITY = 64;

可以看到,以上的几个属性和 HashMap 一模一样,除此之外,比较重要的一个参数就是:

private transient volatile int sizeCtl;

在计算出数组长度、也就是装整个 Map 的那个 Node[] table 的长度之后,是赋值给 sizeCtl 这个元素的。

如果说使用无参构造,那么初始化的时候,sizeCtl 会是 16,其他的情况会计算成为一个 2 的幂次方数,也和 HashMap 是一样的。另外,sizeCtl 这个参数的值还有别的含义:

负数代表正在进行初始化或扩容操作

-1代表正在初始化

-N 表示,这个高16位表示当前扩容的标志,每次扩容都会生成一个不一样的标志,低16位表示参与扩容的线程数量

正数或 0,0 代表 hash 表那个数组还没有被初始化,正数表示达到这个值需要扩容(扩容阈值,其实就等于(容量 * 负载因子),也就是数组长度*0.75)。

还有一部分基本是和扩容相关的属性,第一眼看过去可能不能理解这些什么时候会用,下面讲到扩容方法的时候就会用到:

//扩容相关,每个线程负责最小桶个数 private static final int MIN_TRANSFER_STRIDE = 16; //扩容相关,为了计算sizeCtl private static int RESIZE_STAMP_BITS = 16; //最大辅助扩容线程数量 private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; //扩容相关,为了计算sizeCtl private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; //下面几个是状态值 //MOVED表示正在扩容 static final int MOVED = -1; // hash for forwarding nodes //-2表示红黑树标识 static final int TREEBIN = -2; // hash for roots of trees static final int RESERVED = -3; // hash for transient reservations //计算Hash值使用 static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash //可用CPU核数 static final int NCPU = Runtime.getRuntime().availableProcessors(); //用于记录容器中插入的元素数量 private transient volatile long baseCount; 2.3 jdk 8 的 put() 方法

和 jdk 8 的 HashMap 方法一样,直接调用的是 putVal 方法去执行。

ConcurrentHashMap源码解析,多线程扩容

ConcurrentHashMap源码解析,多线程扩容

计算出 hash。(hash值的计算是通过hashCode进行spread方法的再次计算,一定是一个正数,也是后面再次计算取模得到在table中位置的依据)

判断是否需要进行初始化,也就是第一次put 的时候将容器进行初始化,初始化调用的是 initTable 方法。(这个方法里面利用到了上面的 sizeCtl 参数,通过 CAS 操作来判断是不是有别的线程同时在做初始化,保证只有一个线程在做初始化的操作,没有加锁

f 即为当前 key 定位出的 Node,node 的位置就是通过括号里面的 tabAt 计算的,如果为空表示当前位置,也就是数组的这个位置是个空,可以写入数据。也是利用 CAS 尝试写入,失败则自旋保证成功,可以看到这里,因为定位到的那个 Node 是个空链表,所以就直接利用了 CAS 操作(也没有加锁

那如果不是空,就进行到下面的 else if,如果判断哈希值 == MOVED,代表数组正在扩容,那么就会进行 helperTransfer 方法进行协助扩容,因为没办法继续put了

否则进入下一个 else if ,这里是jdk11有,但是8是没有的,这里用到了OnlyIfAbsent变量,实现的是而 putIfAbsent,也就是在放入数据时,如果存在重复的key,那么putIfAbsent不会放入值(并不像put 那样覆盖)。

否则进入下一个 else,也就是不属于上面任何一个特殊情况的插入,需要遍历这里面的链表进行插入,可以看到利用了 synchronized加锁 然后,遍历链表写入数据,那如果不是链表,是树节点,就走另一个分支去遍历插入。插入完成之后,就常规的将元素个数+1 并结束,那么+1的时候调用的是 addCount 方法,这个方法就涉及到可能会扩容,下面有详细讲解。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpswjs.html