-
ConcurrentHashMap扩容时实现线程安全的原理
- jdk1.7以及以前的情况是分段加锁
- jdk1.8做了一些优化和改进,根据桶位进行synchronized关键字+CAS.,当桶位为null时使用cas操作,当数组中存在数据,则使用synchronized关键字对数组的桶位进行加锁。
-
ConcurrentHashMap重要变量
- sizeCtl存在4种情况,是volatile关键字
- -1:表table正在初始化
- 除了-1的其他负数:其低16位正在进行数据迁移的线程数取反-1
- 0:表table是null,表是需要创建
- 其他正数:为这个hashMap下次进行resize的元素阈值(即key的个数)
- 改变该值的在5个重要方法
- initTable()
- addCount()
- tryPresize()
- transfer()
- helpTransfer()
- sizeCtl存在4种情况,是volatile关键字
接下来逐个解析各个方法
-
构造方法
构造concurrentHashMap不传参数时,默认大小为16
public ConcurrentHashMap() {
//什么也没有干,主要工作在initTable()
}
带参数构造方法 整个方法只做了一件事,计算出sizeCtl的值
//根据用户传入参数,如果小于0则报错, 如果大于最大值则取最大值,否则传入tablesizefor()方法,该方法意义是获得一个数,该数是2的倍数且和传入值最近。将上述值传给sizeCtl
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
-
put方法。
当我们构造了一个ConcurrentHashMap后使用put方法防值时,它的底层发生了什么,上源码
public V put(K key, V value) {
return putVal(key, value, false);
}
//onlyIfAbsent表明如果key存在是否插入值
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//参数校验
if (key == null || value == null) throw new NullPointerException();
//通过对象的hashCode获取一个hash值
int hash = spread(key.hashCode());
//用于记录相应链表的长度
int binCount = 0;
//遍历table的Node元素
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果table为空初始化一个table
if (tab == null || (n = tab.length) == 0)
//4.详解数组初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果数组该位置为空,
// 用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了
// 如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//hash居然可以等于Moved,这个需要到后面才能看明白,不过从名字也能猜到
//肯定是因为在扩容
else if ((fh = f.hash) == MOVED)
//帮助数据迁移,这个等到看完数据迁移部分的介绍后,
//再理解这个就很简单了
tab = helpTransfer(tab, f);
else {
//到这里就是说,f是该位置的头结点,而且不为空
V oldVal = null;
//获取数组该位置的头结点的监视器锁
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
//头结点hash值大于0,说明是链表
//用于累加,记录链表的长度
binCount = 1;
//遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树
// 具体源码我们就不看了,扩容部分后面说
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
putVal()一共做了以下5件事
如果table尚未初始化则进行初始化initTable()
如果通过key的hash运算获得在table桶的位置为null,则通过cas操作桶位挂载上put进来的key和value
如果key对应的table桶位的hash值为Moved标识,则代表当前table正在进行扩容,则调用helpTransfer(tab,f)帮助扩容
否则通过synchronized锁桶位进行赋值
当前桶位是链表,且map中存在该key,则进行值替换。否则在链表后添加该<key,value>实体。
如果桶位是红黑树,则通过putTreeVal方法新增叶子节点。
判断当前桶位链表长度是不是大于转换为树的阈值即8,treeifyBin()进行变树,或者进行数组扩容
-
初始化initTable(首先看其他线程是否使用初始化或者resize,没有的话通过cas锁进行初始化)
//根据sizeCtl判断是否正在进行扩容,如果进行正在有其它线程正在对table
//进行扩容,则进行线程等待,再次循环
//否则通过cas将sizeCtl的值改为-1,进行table初始化默认为16,初始化成功将
//sizeCtl的值赋值为0.75的容器大小
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// 初始化的"功劳"被其他线程"抢去"了
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// DEFAULT_CAPACITY 默认初始容量是 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 初始化数组,长度为 16 或初始化时提供的长度
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 将这个数组赋值给 table,table 是 volatile 的
table = tab = nt;
// 如果 n 为 16 的话,那么这里 sc = 12
// 其实就是 0.75 * n
sc = n - (n >>> 2);
}
} finally {
// 设置 sizeCtl 为 sc,一般为12
sizeCtl = sc;
}
break;
}
}
return tab;
}
重点解析:
在初始化方法中首先根据构造方法初始化的sizeCtl或者DEFAULT_CAPACITY生成一个大小相同的Node<k,V>[]数组。
确定进行下一次扩容时的阈值赋予sizeCtl
- 链表转红黑树treeifyBin()
//MIN_TREEIFY_CAPACITY=64
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//数组长度小于64时
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
//进行数组扩容 扩展为原来的2倍
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
//锁桶位
synchronized (b) {
// 下面就是遍历链表,建立一颗红黑树
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
// 将红黑树设置到数组相应位置中
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
treeifyBin首先判断当前数组是否已经达到64,如果未达到则进行扩容,否则将当前桶位的链表转化为红黑树。
- 尝试扩容方法解析
// 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了
private final void tryPresize(int size) {
// c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
// 这个 if 分支和之前说的初始化数组的代码基本上是一样的,
//在这里,我们可以不用管这块代码
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2); // 0.75 * n
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
//如果需要扩容的预期大小,小于当前数组阈值,则跳出循环
break;
else if (tab == table) {
//**生成表的生成戳,每个n都有不同的生成戳
//* static final int resizeStamp(int n) {
// return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
// }
//Integer.numberOfLeadingZeros(n)在指定 int 值的从左边起连续0的数量
// 例如 n为16 0001 0000 int32 位 32-5=27
//则Integer.numberOfLeadingZeros(n)为27,
//然后与(1 << (RESIZE_STAMP_BITS - 1)) | ,相当于2^15 | n中0的个数。
//(因此rs 左移16位后符号位为1,结果肯定是个负数)
int rs = resizeStamp(n);
if (sc < 0) {
Node<K,V>[] nt;
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);
}
// sizeCtl高16位代表扩容的标记、低16位代表并行扩容的线程数+1
// 调用 transfer 方法,此时 nextTab 参数为 null
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
如果需要扩容的预期大小,小于当前数组阈值,则跳出循环
如果需要扩容则分两种情况
已经有一个线程在扩容,或者在初始化,则生成戳,协助数据迁移的小于线程最大值,transferIndex 是小于等于0,而break。否则进行cas操作修改sizeCtl值,并调用transfer进行协助数据迁移。
如果没有线程在扩容则cas修改sizeCtl.sizeCtl高16位代表扩容的标记、低16位代表并行扩容的线程数+1,调用transfer进行数据迁移
-
数据迁移(重点)
在concurrenthashmap进行迁移当中,因为可能会存在多线程并发的问题。Doug Lea将迁移过程分成多个小任务分给多个线程来做,使用了stride:步长。transferIndex:全局调度者
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//stride在单核下直接等于n,多核模式下为(n>>>3)/NPC,最小值是16
//stride可以理解为"步长",有n个位置是需要进行迁移的
//将者n个任务分为多个任务包,每个任务包有stride个任务
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//如果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;
transferIndex = n;
}
int nextn = nextTab.length;
//
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
//cas锁划分一小段,多线程同时通过cas锁划分段落 给i,bound赋值
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//判断该线程是否完成
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//所有线程完工
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//判断是不是所有线程都完工了
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 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)
//如果当前桶位为空,则赋予一个fwd节点,该节点的hash是Moved
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
//对链表进行数据迁移
if (fh >= 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 (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
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);
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;
}
}
//如果迁移后的树小于阈值6,则转化为链表
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);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
-
helpTransfer(Node<K,V>[] tab,Node<K,V>f)
当前线程进行桶位赋值时候,发现了一个forwardingNode节点(即f.hash为Moved),将该节点和table传入helpTransfer()方法中。
这个方法会经过一系列判断后(判断情况说明在addCount和tryPresize有说明不在赘述)调用transfer方法进行数据迁移
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//nextTab 引用的是 fwd.nextTable == map.nextTable 理论上是这样。
//sc 保存map.sizeCtl
//ForwardingNode节点的hash是MOVED
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
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;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
-
addCount(long x,int check)
counterCells和baseCount都维护这整个Map的个数
当总节点数量超过sizeCtl则对数组执行扩容操作, 将数组长度扩大为原来的2倍;
当添加、修改 元素都会触发判断数组是否需要扩容的操作:sizeCtl作为判断是否对数组进行2倍扩容的依据;
private final void addCount(long x, int check) {
//CounterCell是数据对齐的long[] 数组as,
//这里使用了LongAdder的并发操作,可以使多个线程同时+1或者-1
//baseCount也是维护值整个Map的元素个数
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
//1.counterCells不为空
//2.对BaseCount进行+x操作失败了
CounterCell a; long v; int m;
boolean uncontended = true;
// 如果计数盒子是空(尚未出现并发)
// 如果随机取余一个数组位置为空 或者
// 修改这个槽位的变量失败(出现并发了)
// 执行 fullAddCount 方法。并结束
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//如果as==null
//as的长度为小于等于0
//当前线程进行+x失败。则调用fullAddCount
fullAddCount(x, uncontended);
//这个方法通过cas、初始化、扩容等一系列方式将x加上
return;
}
//put值时,这个桶位以前数量为小于1,不需要扩容
if (check <= 1)
return;
s = sumCount();
}
//以下部分判断是否需要扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 如果map.size() 大于 sizeCtl(达到扩容阈值需要扩容) 且
// table 不是空;且 table 的长度小于 1 << 30。(可以扩容)
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//扩容时间戳
int rs = resizeStamp(n);
if (sc < 0) {
// 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
// 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)
//(默认第一个线程设置 sc ==rs 左移 16 位 + 2,
//当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
// 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
// 如果 nextTable == null(结束扩容了)
// 如果 transferIndex <= 0 (转移状态变化了)
// 结束循环
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 如果可以帮助扩容,那么将 sc 加 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();
}
}
}