一、非常重要的sizeCtl属性
initTable()方法的作用是初始化哈希表,初始化哈希表就要有确定哈希表容量、创建哈希表并将哈希表的引用赋值、修改哈希表的阈值等步骤。initTable()方法里面采用了不加锁方式来确保在高并发的环境下创建哈希表的全部步骤都只能由一个线程完成。
Concurrenthashmap源码中有控制标识符“sizeCtl”,它的值代表了目前哈希表的状态:
1、如果sizeCtl = 0,表示哈希表未初始化,并且数组的初始容量是16;
2、如果sizeCtl = -1,表示哈希表正在进行初始化;
3、如果sizeCtl < 0并且sizeCtl != -1,表示哈希表正在扩容,-(1+n)的值为正在完成数组扩容的线程数量;
4、如果sizeCtl > 0,则有两种情况,一是表示如果哈希表未初始化,但是创建则Concurrenthashmap对象时使用了带参构造传入了初始容量,Concurrenthashmap会将初始容量重新计算并且使用sizeCtl 来记录这个初始容量;二是哈希表已经初始完成,则记录的是哈希表的扩容阈值;
相关代码如下:
public ConcurrentHashMap(int initialCapacity) {
//如果传入的初始容量小于0,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
//计算大于初始用量的最小的2的幂作为初始容量
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
//使用sizeCtl来保存
this.sizeCtl = cap;//初始化sizeCtl
}
二、initTable() 方法的源码解析
先来看initTable()方法的代码:
/*
* sizeCtl释义:
* 值为0,表示数组未初始化,并且数组的初始容量是16
* 为正数,表示如果数组未初始化,则记录的是数组的初始容量;如果数据已经初始化,则记录的是数组的扩容阈值
* 为-1,表示数组正在进行初始化
* 非-1的负数,表示数组正在扩容,-(1+n)的值为正在完成数组扩容的线程数量
* */
private final Node<K, V>[] initTable() {
Node<K, V>[] tab;
int sc;
//第①步,判断数组是否未初始化
while ((tab = table) == null || tab.length == 0) {
//第②步,用sc保存sizeCtl的值,作为后面CAS的预期值
//第③步判断sizeCtl的值是否<0,是的话则发现有其他线程在做数据的初始化,让出CPU
if ((sc = sizeCtl) < 0)
Thread.yield();
//走到这里,说明sizeCtl的值大于或等于0,则有两种清况:一是数组未初始化。
//二是有其他线程在第①步之后,第②步之前将数组初始化完成,此时sizeCtl为数组的扩容阈值.
//第④步,CAS将SIZECTL值修改为-1,表示本线程开始进行数组的初始化
//如果修改成功,开始初始化操作;如果修改失败,则表示有其他线程在①②之后抢先修改了SIZECTL
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
/*第⑤步,double check数组是否初始化,这里为什么要采用双重校验呢?
*因为在数组初始化完成之后,sizeCtl的值会,被改成数组的扩容阈值,会是一个大于0的值。
*所以完全有可能本线程在①之后,有其他线程完成了数组的初始化全过程,
* 使得本线程也能进到这个代码块里来。
*/
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/4,作为扩容的阈值,赋值给sc
sc = n - (n >>> 2);
}
} finally {
//第⑦步,如果初始化完成,sizeCtl记录的是扩容阈值;
//如果初始化失败,则还原sizeCtl
sizeCtl = sc;
}
//走到这里,说明本线程初始化完成了或者其他线程在本线程的
//①、②两步之间完成了哈希表的初始化全过程,此时结束循环
break;
}
//走到这里,说明在发现数组未初始化之后,准确初始化之前,
//有其他线程已经抢先开始初始化了
//但是其他线程是否将初始化的工作全部正确的完成,并不知道,所以重新开始循环检查
}
return tab;
}
三:initTable() 方法的步骤解析
根据上面的源码,initTable() 方法的详细步骤为:
①判断哈希表是否未初始化,未初始化的话进入第②步。
②记录sizeCtl的值,用作后面CAS的预期值。
③判断sizeCtl的值是否小于0。如果sizeCtl<0则表示有其他线程在本线程执行步骤①之后抢先执行了第④步将sizeCtl的值改为-1,此时本线程则让出CPU。如果sizeCtl >= 0,则有两种可能,一是哈希表未初始化;二是有其他线程在本线程执行步骤①之后已经抢先将哈希表初始化完成,此时此时sizeCtl记录的是哈希表的扩容阈值自然也是大于0的;这两种情况都进入下一步。
④CAS将SIZECTL值修改为-1。如果修改成功,开始初始化操作;如果修改失败,则表示有其他线程在本线程执行③之后抢先修改了SIZECTL为-1,此时重新回到步骤①检查哈希表是否初始化。
④再次哈希表哈希表是否初始化,这里为什么要采用双重校验呢?因为在哈希表初始化完成之后,sizeCtl的值会被改成数组的扩容阈值,满足sizeCtl >= 0。所以完全有可能本线程在①之后②之前,有其他线程完成了数组的初始化全过程,使得本线程也能进到这里来。
⑤如果哈希表还未初始化,则开始创建哈希表等工作。如果哈希表已经初始化,则还原sizeCtl的值。
⑦如果初始化完成,sizeCtl记录扩容阈值。
下面是笔者自己画的流程图: