JUC全文
ConcurrentHashMap-jdk1.8
- 底层也是调用
unsafe
的方法,进行CAS操作
初始化
- 空构造采用懒加载的形式,只有添加第一个元素的时候才真正初始化
- 有参构造,推荐使用,防止使用过程中扩容,影响性能
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
//获取大于传入的数的最近2的整数次幂
//传入为initialCapacity * 0.5 initialCapacity + 1
//则获取出来的是 > initialCapacity 的2的整数次幂 传入32 获取64
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
sizeCtl
,记录多线程场景下,初始化和扩容的阶段
- 为0,数组未初始化,默认的初始容量为16
- 为正数,如果数组未初始化,记录数组的初始容量,如果数组初始化,记录数组扩容的阈值
(容量*0.75)
- 为-1,数组正在进行初始化
- <0,且不是-1,数组正在扩容,
sizeCtl = Integer.MIN_VALUE + 2 + n
表示有n个线程正在共同完成数组的扩容
put
中真正初始化initTable
,CAS自旋操作进行,避免真正上锁
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) //小于0,正在初始化/扩容,有其他线程正在操作
Thread.yield(); // 释放掉cpu,不争抢,由执行状态,变为就绪
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //将sizeCtl设置为-1,设置失败后重新循环,让出CPU
try {
//再次判断,double-check,避免重复初始化
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //默认16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2); //设置阈值 sc = size - size / 4 = size * 0.75
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
put
- 得出的哈希值一定是个正数,方便后面添加元素判断该节点的类型
int hash = spread(key.hashCode());
- 一个死循环
- 如果tab中当前角标没有值,在当前角标创建一个
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
}
- 判断哈希值是否为
MOVED -1
,代表此时正在扩容,帮助扩容
会在角标处放一个
forward
节点,其哈希值为-1
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
- 开始添加操作,锁对象为当前角标的对象,不影响其他的角标的操作
if (tabAt(tab, i) == f) {
double-check,防止添加前,其他线程进行了扩容,使得位置改变- 哈希值大于0,为一个普通的链表结构,为一个单向链表
- 尾插法
- 树结构的插入
- 插入操作完毕,
binCount
记录节点数
if (binCount != 0) {
//节点数>=8 进行树化
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null) //如果是替换了,直接返回旧值
return oldVal;
break;
}
- 树化中,如果长度小于64,会用扩容解决
维护集合长度
put
操作之后需要,维护集合长度,并检查是否需要扩容
addCount(1L, binCount);
addCount
- 尝试对
baseCount
++,成功即完成- 尝试对
ConterCell
数组中的某个角标进行++,直到成功,其角标通过线程哈希得到- 尝试对
ConterCell
数组++的过程中,如果失败,会对线程重哈希,换个角标进行
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//维护集合长度
if ((as = counterCells) != null || //不为空的时候必然给CounterCell++
//对baseCount进行尝试++
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
//累加失败 对CounterCell[]数组中++
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 || //数组为空
(a = as[ThreadLocalRandom.getProbe() & m]) == null || //当前角标为null
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { //尝试放置
//实际添加动作
fullAddCount(x, uncontended);
return;
}
if (check <= 1) //当前角标只有一个节点,或者当remove时传入-1时,直接返回,不考虑扩容
return;
s = sumCount();
}
if (check >= 0) { //扩容,添加的时候才会满足这个条件
Node<K,V>[] tab, nt; int n, sc;
//如果大于阈值,且不为null,不越界
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//获取标记,1在第16位的位置,后面有15个0
int rs = resizeStamp(n);
if (sc < 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)) //此时sc+1
//协助扩容
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
//rs左移16位,后面31个0,相当于1<<31,为一个负数
(rs << RESIZE_STAMP_SHIFT) + 2))
//当前线程开始扩容
transfer(tab, null);
s = sumCount();
}
}
}
fullAddCount
具体工作
- 拿到数组的随机值,计算CounterCell数组的角标
- 如果不存在CounterCell数组,尝试为其初始化,初始容量为2,需要尝试去对
cellsBusy
设置值,并且一次在初始化时只能有一个线程对cellsBusy
设置- 尝试为其初始化失败,则再次尝试对
baseCount
++,设置成功后跳出- 初始化成功后,所有线程都将对
CounterCell
数组设置,当前线程的角标没有CounterCell
对象,将x封装成CounterCell对象的value值中,设置成功后跳出- 如果有,尝试给该角标对象加x,成功则跳出
- 失败,对CounteCell数组进行扩容,尽量避免扩容,当数组长度大于cpu核数或仅仅是因为没抢到
cellsBusy
- 扩容后,对线程重哈希,重新找角标
resizeStamp
,标记,标识开始扩容的状态,如果后续回到这个标识,则代表扩容完成
static final int resizeStamp(int n) {
//1左移15位
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
集合长度累积
sumCount
,累计baseCount
和CounterCell
数组
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
扩容
- 多线程协助扩容的时间点
- 尝试添加元素时,发现当前角标的元素为
fwd
- 添加完之后,
addCount
维护集合长度时,发现sc
标志位<0
transfer
扩容,并迁移数据- 将
旧数组
划分为几块,计算任务单元,最小是16个
//cpu<=1 交给一个线程
//cpu>1 右移三位 /8
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
//如果小于16,那么就为16
stride = MIN_TRANSFER_STRIDE; // subdivide range
- 新建数组
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
//新数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n; //旧数组的最大下标
}
- 每一个迁移的节点,赋值一个
ForwardingNode
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
- 从后往前开始迁移,分配任务
bound
任务的边界,通过nextBound
获得i/nextIndex
本次任务开始的下标
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
//i往前移动,还没有达到下一个次任务的边界,跳出while,进行当前任务的下一次迁移
advance = false;
else if ((nextIndex = transferIndex) <= 0) { //已经没有任务可以接了,那么直接为-1
i = -1;
advance = false;
}
//任务分配
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
//更新边界
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound; //记录下一次边界
i = nextIndex - 1; //本次任务的开始下标
advance = false;
}
}
- 迁移是否完成的逻辑,包含全部完成和单个线程完成,单个线程完成时可以进入
//i为当前任务的下标 n为旧数组的长度 nextn为新数组的长度
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { //全部完成
nextTable = null;
table = nextTab;
//更新阈值 新size*0.75
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//将sc设置为-1,当有线程协助扩容时,会令sc+1,当前线程完成后,sc-1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//sc为 (rs << RESIZE_STAMP_SHIFT) + 2) rs在第31位+2,不相等时,扩容还未完成
//当前线程先return,下次在此进入
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
- 当前位置为null,直接放
fwd
;当前位置已经是fwd
不迁移,继续下一个任务
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
- 真正迁移,为当前位置上锁,跟添加是一个锁
- 单向链表的迁移过程与
1.7
类似
- 先找链表中末尾,连续的,可以放在同一个位置的链表直接移动过去
- 处理其他节点,通过
hash & 旧长度
快速得到放在原下标还是新下标- 复制一份,头插法加入
- 迁移过的原数组的角标放上
fwd
- 树的迁移过程与
hashMap
类似,借助双向链表的结构迁移
- 将链表中的高低位分开
- 判断是否需要转换回单向链表
- 如果其中拆分了,重新树化,用的还是原节点
结构
ForwardingNode
- 继承了
Node
,传入-1 MOVED
的哈希值
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
TreeBin
- 红黑树节点哈希为
-2
- 内部包含一个
TreeNode
实际操作,通过TreeNode
进行操作
remove
public V remove(Object key) {
return replaceNode(key, null, null);
}
- 步骤
- 计算当前角标,如果当前角标为null,直接返回
- 当前角标位置为
fwd
,协助迁移- 上锁,锁对象为当前角标对象,遍历当前角标的单向链表/红黑树
- 找到有一个有效的节点 && 旧值不为null && 传入的value为null,维护集合长度
内部调用
replaceNode(key, null, value)
,第二个参数为旧值,传入null,代表要删除
get
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) { //当前角标的元素
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0) //当前角标为红黑树,find查找,二叉搜索树的查找,由hash构建
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) { //遍历单向链表
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
size
- 调用
sumCount
,累计baseCount
和CounterCell
数组,这两个会在addCount
中维护
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
小结
- 结构
tab[]+单向链表/红黑树(双向链表)
- key完全相等的条件
- 哈希值相等
- 指向同一块内存区域/值相等,或者,
equals
方法使其相等
- 保证线程安全
- 针对某一个角标上锁,
put/remove/transfer
- 在发现一个节点为
fwd
时,会协助其他线程扩容- 扩容动作时,将整个扩容区间分为最小为16的任务单元,由每个线程分别领取,线程完成当前任务后,会跳出
- CAS操作
- 集合长度
- 添加元素完成后,会实时维护继承长度,通过
baseCount+countercells
的形式,分别保存节点数量,通过sum
计算- 获取结合长度直接通过
sumCount()
即可获得