一、 jdk1.8容器初始化
1.1 源码分析
//没有进行任何操作,在添加数据时才会创建数组
public ConcurrentHashMap() {
}
//initialCapacity:指定数组初始长度(创建出的数组长度不是这个值)
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
//MAXIMUM_CAPACITY = 1 << 30,正常情况下,数组长度不会指定这么大,因此程序进入tableSizeFor()
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
//方法内的参数表示变为初始容量的1.5倍+1
//举例:当初始容量为16时,最终参数的值为25
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
//cap表示真正的数组初始长度,将其赋值给sizeCtl
//sizeCtl的含义下文中会介绍
//此时,只是计算出数组的初始长度,并没有创建出数组
this.sizeCtl = cap;
}
//将传进来的参数变为2的次幂
//具体实现是通过位或运算使其所有位都变为1(举例:11000 -> 11111)
//举例:当c=25时
private static final int tableSizeFor(int c) {
//n=24
int n = c - 1;
//n=24二进制表示为11000,n右移一位为01100,位或操作后n=28(11100)
n |= n >>> 1;
//28二进制表示为11100,n右移2位为00111,位或操作后n=31(11111)
n |= n >>> 2;
//31二进制表示为11111,n右移4位为00001,位或操作后n=31(11111)
n |= n >>> 4;
//下边的指令执行完后,n还为31
n |= n >>> 8;
n |= n >>> 16;
//n+1==32
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
数组长度为什么是2的n次幂,可以查看我的上一篇文章: ConcurrentHashMap的容量为什么是2的n次幂.
//传入的参数为一个map集合,基于这个集合创建一个concurrentHashMap
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
//将sizeCtl赋值为默认初始容量16
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
1.2 sizeCtl含义解释
sizeCtl = 0,数组未初始化,数组默认长度为16
sizeCtl > 0,数组初始化前,表示数组长度,数组初始化后,表示扩容的阈值
sizeCtl = -1,数组正在初始化或扩容
sizeCtl < 0 且 不等于-1,sizeCtl的高16位表示扩容标识戳,低16位表示帮助扩容的线程数量+1
二、jdk1.8添加安全
2.1 源码分析
2.1.1 添加元素put/putVal方法
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//不允许空键空值
if (key == null || value == null) throw new NullPointerException();
//根据key的hashCode方法得到hash值,再通过扰动算法使散列表分布均匀,得到最终的hash值
//计算出的hash值肯定为正值,方便后面添加元素判断节点类型
int hash = spread(key.hashCode());
//统计每个桶上的元素个数,如果超过8个,会转换成红黑树
int binCount = 0;
//死循环,只能通过break跳出循环
for (Node<K,V>[] tab = table;;) {
//f:当前命中的数组索引位置上的元素
//n:数组长度
//i:数组索引位置
//fh:f的hash值
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) {
//以CAS方式在tab[i]上创建一个Node节点,节点中存储了hash值,key,value值,并且next指向null。
//添加成功,跳出循环。
//添加失败,说明有其他线程已经添加进去值了,只能重新进入for循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//先决条件:数组已经创建好了且当前命中的数组元素不为空
//当前命中的数组元素hash值为-1,说明正在扩容
else if ((fh = f.hash) == MOVED)
//当前线程协助扩容
tab = helpTransfer(tab, f);
//先决条件:数组已经创建好了,当前命中的数组元素不为空,且数组没有在扩容
//接下来就要形成链表
else {
V oldVal = null;
//f:当前线程命中的数组元素
//对该元素单独加锁,不会影响数组中其他元素的任何操作
synchronized (f) {
//防止其他线程添加数据后,使当前位置变为红黑树结构(红黑树结构会通过左旋或右旋改变该位置元素),
//或该数组被其他线程扩容了,再次确认该位置上是否还是原来的元素
if (tabAt(tab, i) == f) {
//hash值>=0,表示是链表结构
if (fh >= 0) {
//标记该位置上元素个数为1
binCount = 1;
//遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//判断当前节点与插入节点的key是否相等
//true:将插入节点的value覆盖当前节点的value
//false:判断当前节点的下一个节点是否为空
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
//插入节点的value覆盖当前节点的value
e.val = value;
//跳出循环
break;
}
//下面开始判断当前节点的下一个节点是否为空
//Node<K,V> pred = e和e = e.next组合起来是将下一个节点设置为当前节点
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
if (binCount != 0) {
//如果链表个数>=8,则将其转化为树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
//返回被覆盖的旧值
if (oldVal != null)
return oldVal;
break;
}
}
}
//数据添加完成后,要维护数组长度,还要判断数组是否需要扩容
addCount(1L, binCount);
return null;
}
2.1.2 数组初始化,initTable()方法
//数组初始化。创建数组
private final Node<K,V>[] initTable() {
//tab:数组
//sc:期望值
Node<K,V>[] tab; int sc;
//再次确认数组是否为空,防止其他线程已经创建好数组
while ((tab = table) == null || tab.length == 0) {
//因为此时还未创建好数组,所以数组不可能在扩容
//true:sc = -1,数组正在初始化
//false:sc = 0或sc = 数组初始容量(根据上面分析构造器那块可知)
if ((sc = sizeCtl) < 0)
//当前线程放弃
Thread.yield(); // lost initialization race; just spin
//以CAS方式修改sizeCtl的值,修改成功,sizeCtl = -1,下面开始正式初始化操作
//修改失败,其他线程正在初始化,重新进入循环
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//其他线程可能已经执行过初始化,使sizeCtl = sc=数组扩容时阈值,
//这样的话,也可以CAS修改成功,再次判断可以防止重复初始化
if ((tab = table) == null || tab.length == 0) {
//sc > 0:根据传入的参数将数组初始容量转换为2的n次幂
//sc = 0:数组初始容量为默认值16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//数组长度为默认长度16
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//sc = 0.75 * n,表示数组扩容时的阈值
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
//初始化完成后,跳出循环
break;
}
}
return tab;
}
2.2 图解
三、jdk1.8扩容安全
3.1 addCount():数据添加完成后,要维护数组长度,还要判断数组是否需要扩容的方法
//数据添加完成后,要维护数组长度,还要判断数组是否需要扩容
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;
//计数统计,将base和cell[]中的值都加起来
s = sumCount();
}
//==========以上属于对添加进去的元素计数统计,借用了LongAdder的源码===================
//======================以下是判断是否需要扩容的代码===============================
//如果是添加数据,一定大于0
if (check >= 0) {
//tab:旧数组
//nt:扩容后的新数组
//n:数组长度
//sc:期望值
Node<K,V>[] tab, nt; int n, sc;
//true:条件1-> 超过扩容阈值或sizeCtl为负数表示正在扩容
//条件2-> 数组创建好了 条件3-> 数组长度没有达到最大允许值,意味着可以扩容
//false:直接退出,不需要扩容了
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//生成扩容标识戳
int rs = resizeStamp(n);
//负数,表示有线程正在扩容,sizeCtl的高16位是扩容标识戳,低16位表示扩容线程数量+1
if (sc < 0) {
//条件1:当sc为负数时,右移16位(即sc的高16位),表示扩容标识戳,但条件1为true时,说明数组已经扩容完成了
//条件4:新数组为空
//条件5:transferIndex <= 0
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//CAS方式修改sizeCtl的值,修改成功,sizeCtl+1,表示扩容的线程数量+1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
//协助扩容
transfer(tab, nt);
}
//sc表示扩容阈值
//以CAS方式修改sizeCtl的值,扩容标识戳左移16位+2
//在下面的例子中,rs = 1000 0000 0001 1011,
//如果修改成功,sizeCtl的高16位是扩容标识戳,低16位表示扩容线程数量+1
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//扩容操作
transfer(tab, null);
//计数统计
s = sumCount();
}
}
}
LongAdder源码分析可参考我的文章: LongAdder源码讲解.
3.1.1 扩容标识戳生成方法
//生成扩容标识戳
static final int resizeStamp(int n) {
//numberOfLeadingZeros(n):返回无符号整数n最高位非0位前面的0的个数
//1 << (RESIZE_STAMP_BITS - 1):1左移15位
//举例:数组默认长度是16,二进制表示是10000,1前边27个0,27用二进制表示为11011
//1左移15位用二进制表示1000 0000 0000 0000,这个操作会使sizeCtl的最高位为1,sizeCtl就成了负数
//返回结果是1000 0000 0001 1011
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
3.2 transfer():扩容操作
3.2.1 扩容基本原理
在多线程环境下,为了提高扩容效率,会把一个数组分割成几块,交由几个任务来迁移,每个任务的最短长度是16。具体实现过程是数组每分割出一块,就由一个线程来完成这一任务。
3.2.2 任务分配图解
为每个线程分配任务的过程图解
在本图中,如果最后一个任务分得的长度<4,则把剩下的当成一个任务来迁移。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
//n:旧数组的长度
//stride:步长
int n = tab.length, stride;
//单核cpu情况下,步长为n,多核cpu下,步长最短为16,如果大于16为数组长度/(8 * cpu核数)
//可以理解成一个长度为n的数组需要扩容,将数组分成几块来迁移,每一块的长度为stride,stride最小为16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//当执行transfer(tab, null)这个方法时,即第一次发起数据迁移时,nextTab == null
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;
//指向原数组最后的位置+1
transferIndex = n;
}
//新数组的长度
int nextn = nextTab.length;
//建一个forwardingNode节点,用来标记该位置已经被迁移了
//标记了fwd标志的位置,对应的hash值为-1
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;
//true->当前线程分配的迁移工作已完成
//=============while这一块代码是分配任务的,真正迁移是在下边============================
while (advance) {
int nextIndex, nextBound;
//--i >= bound:当前线程分配的迁移任务还没有完成
if (--i >= bound || finishing)
advance = false;
//所有数组元素都被分配完了
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//第一次进来时,只能进下边这个判断
//如果剩下的任务大于stride,下一个边界等于nextIndex-stride
//如果剩下任务不够一个步长,只能将下一个边界指向0
//分配任务,以CAS方式修改transferIndex的值,
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//进行下一个任务分配的准备
//将下一个边界赋值给当前边界
bound = nextBound;
//正在迁移的数组元素索引为nextIndex - 1,表示当前任务从这个索引位置开始迁移
i = nextIndex - 1;
//标记迁移还未完成
advance = false;
}
}
//=======================分配完任务后,开始迁移数据==============================
//这是最后的判断条件,一开始不会满足这个条件,所有数组元素都被分配了
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//任务分配完了,还需要判断是否扩容完成了
//true->扩容完成了
if (finishing) {
nextTable = null;
//扩容后的数组赋值给当前数组
table = nextTab;
//sizeCtl=2n-0.5n = 1.5n = 0.75 *2n,重新计算扩容后的阈值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//所有数组元素都被分配了,当前线程没用了,所以当前线程会退出,sizeCtl-1,线程数量-1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//sizeCtl在第一次迁移前,会通过resizeStamp(n) << RESIZE_STAMP_SHIFT + 2计算出sizeCtl,
//当有线程来协助扩容时,sizeCtl+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)
//不用迁移,直接以CAS方式修改为fwd节点
advance = casTabAt(tab, i, null, fwd);
//当前线程命中的数组元素的hash值为-1,表示这一块任务已经被迁移了,因此,将advance设置为true
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
//=====================不为空,也不为-1,真正的迁移数据===========================
else {
//只对当前线程命中的数组元素加锁,其他元素的任何操作都不受影响
synchronized (f) {
//再次确认,防止其他线程扩容时将该元素转移到其他位置
if (tabAt(tab, i) == f) {
//当第一次求数组的索引位置时,用hash&(n-1),但当数组扩容时,
//为了提高效率,可以直接判断hash值的第x位。(x的求法举例说明)
//举例:数组长度为16时,n-1用二进制表示为1111,用4位二进制数即可表示,
//当扩容成32时,n-1表示为11111,5位二进制数表示,那么可以直接判断hash值的从低位往高位数第5位是否为0,
//如果为0,该数据还在该位置,如果为1,该数据在该位置+原数组长度。
//ln表示扩容后还在该位置的节点,hn表示在该索引位置+数组长度
Node<K,V> ln, hn;
//表示是链表结构
if (fh >= 0) {
//如果hash值第x位为1,runbit最高位为1,其余位为0
//如果hash值第x位为0,runbit所有位都是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条件是为了让链表最后几个节点如果是同类的话(指他们扩容后索引位置一样),
//不破坏此结构,直接将同类的头节点连接过去
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//==0表示扩容后还在该位置,赋值给ln
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//循环,形成ln链和hn链
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);
//将原数组的i位置设置为fwd节点
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;
//将ln链表放在 i 位置也就是不动
setTabAt(nextTab, i, ln);
//将hn链表放在 i+n 位置
setTabAt(nextTab, i + n, hn);
//将i这个索引位置元素设置为fwd节点,表示已经迁移过了
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
关于为什么能设置ln表示扩容后位置不变,hn表示扩容后原索引位置+原数组长度,可以参考我的文章: ConcurrentHashMap容量为什么是2的n次幂.
3.2.3 链表迁移过程图解
因此,最终,runBit为蓝色,lastRun指向倒数第三个,不破坏最后三个的结构,他们扩容后索引位置一样,直接将头节点连接过去,提高迁移的效率。
四、jdk1.8多线程扩容效率改进
多线程协助扩容会在两个地方被触发:
1、当添加元素时,对应的索引位置是fwd节点,说明正在扩容,那么协助扩容,协助扩容后再添加元素
2、添加完元素后,如果sizeCtl为负数且数组不为空,那么协助扩容。
//第一种情况:
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
}
//先决条件:数组已经创建好了且当前命中的数组元素不为空
//当前命中的数组元素hash值为-1,说明正在扩容
else if ((fh = f.hash) == MOVED)
//当前线程协助扩容
tab = helpTransfer(tab, f);
//第二种情况:
private final void addCount(long x, int check) {
........
........
//======================以下是判断是否需要扩容的代码===============================
//如果是添加数据,一定大于0
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//true:条件1-> 超过扩容阈值或sizeCtl为负数表示正在扩容
//条件2-> 数组创建好了 条件3-> 数组长度没有达到最大允许值,意味着可以扩容
//false:直接退出,不需要扩容了
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//生成扩容标识戳
int rs = resizeStamp(n);
//负数,表示有线程正在扩容,sizeCtl的高16位是扩容标识戳,低16位表示扩容线程数量+1
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//CAS方式修改sizeCtl的值,修改成功,sizeCtl+1,表示扩容的线程数量+1
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();
}
}
}