上文我们提到了ConcurrentHashMap1.7的一些关键知识点,现在我们来了解下ConcurrentHashMap1.8又是怎么实现的呢?
ConcurrentHashMap1.8由于我们的Synchronized优化后,它不在使用1.7的分段锁了而是直接使用Node数组+链表+红黑树的结构,主要利用–> 优化的synchronized + Unsafe(cas)操作实现并发安全。
直接进入主题。
ConcurrentHashMap1.8 put过程是什么样的呢?
1、进来先并发安全的初始化一个容量为16的数组
2、根据key找到对应的数组下标,判断有没有元素,没有就自旋的方式通过cas赋值。
3、然后synchronized加锁,加锁成功。
4、判断类型: value是用volatile修饰,保证了所有线程的可见性。
判断是链表就添加节点到链表。
是红黑树就添加节点到红黑树。
5、根据bigcount个数判断是否要树化
6、进入addcount方法,并发安全的记录count,线程会去竞争baseCount和counterCells数组下的value值,如果没有counterCells数组就会去创建,最后将baseCount和所有value相加统计。
7、同时一个线程发现是ForwardingNode对象,也就是正在扩容则会去帮助扩容。
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();
//线程安全1、进来数组不为空,自旋,通过cas插入内存,旧的预期结果为null
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
}
//两个线程同时扩容,
else if ((fh = f.hash) == MOVED)
//帮助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//线程安全2、链表,插入元素,加锁
synchronized (f) {
//重新检查下f是否发送变化,发生变化就重来
if (tabAt(tab, i) == f) {
//链表
if (fh >= 0) {
binCount = 1;
//++binCount记录下链表有多少个元素
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;
}
}
}
}
//树化不再锁里面,那其他线程进来put,它正在树化呢,
//treeifyBin里面加了锁
if (binCount != 0) {
//上面是链表的话肯定不成立,binCount个数超过了就变树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//记录下binCount类似size,真正扩容
addCount(1L, binCount);
return null;
}
并发安全初始化数组,采用compareAndSwapInt,只有一个线程可以初始化,然后Thread.yield();释放线程
//从主内存拿sizeCtl,默认为0
private transient volatile int sizeCtl;
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
//多个线程,只允许一个线程更改SIZECTL-1
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
//取默认初始化容量16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//初始化数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//sc=n-1/4n=0.75n
sc = n - (n >>> 2);
}
} finally {
//扩容的阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
ConcurrentHashMap1.8是如何统计count个数的?
它使用counterCells,进行统计,灵活运用了并发的原理,让统计的工作分配可以使用其他线程进行辅助统计,那我们来看看addcount方法做了什么
上部分主要是counterCells是如何使用并发环境下辅助统计count的,
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//counterCells不是空并且可以修改baseCount值
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//ThreadLocalRandom.getProbe() & m
//随机数 &length-1
if (as == null || (m = as.length - 1) < 0 ||
//a取到随机索引处的CounterCell
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
//cas修改对应CounterCell对象value值
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//修改value失败说明没有CounterCell数组再走这,x是传进来的1L,false
//compareAndSwapInt(this, CELLSBUSY, 0, 1)
//cas一个进程,先初始化数组CounterCell,然后value+1
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//大于阈值&小于最大容量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
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))
//扩容
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
counterCells部分的代码
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
//起初counterCells为null走最下面if分支
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
//第一次wasUncontended=false,走到最下面进行随机h
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//修改value
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
//collide=false
else if (!collide)
collide = true;
//collide = true,允许一个进来
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
//是否修改
if (counterCells == as) {// Expand table unless stale
//CounterCell两倍扩容
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}//控制一个进行初始化
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try {
// 检查是否修改
if (counterCells == as) {
//初始化2个
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
//尝试竞争baseCount
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
真正扩容的关键代码在这
//假设stride=2
//nextIndex=4
//nextBound=2
//bound =2
//i=4-1=3 所以这里是2和3的位置
//那第二次循环就是bound=0,i=1,0和1的位置
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
最后我们总结一下1.7和1.8ConcurrentHashMap的知识点
1、ConcurrentHashMap1.7和1.8扩容?
1.7支持多线扩容,前提是segment对象不同,相同的话就会加锁。
扩容条件:(是否大于阈值&数组容量<数组最大容量)2倍扩容
扩容过程–>两倍扩容,遍历旧数组,先遍历到链表的最结尾,根据hash计算出一个索引,把最后面的连续相同索引的节点存入lastrun中直接移到新数组,(有点类似蜘蛛纸牌),然后其他在一个一个转移,头插法添加新的node节点
1.8 支持多线程扩容,扩容性能更好,
扩容条件:新增节点会调用addCount记录元素个数,并检查是否扩容(是否大于阈值&数组不为空&数组容量<数组最大容量)
扩容过程–>a: 根据步长会计算出需要转移的位置,转移过程中间会加锁,生成farwardingNode对象。
b: 转移完成就会向前推进,由又向左,(其他位置没有进行扩容的,可以进行正常的put操作),当自己线程把自己需要转移的元素都转移完了,就会去帮助扩容,如果没有就退出转移过程,进入等待,因为其他进程可能还在扩容,等全部结束了,最后一个扩容就会把新数组赋值给table,成功后就能进行put的操作了。
转移加锁,是链表还是树,都是ln(i)和hn(i+oldCap)分好,然后一次性存入新数组。
2、jdk1.8内部中增加CounterCell来帮助计数。而jdk1.7是通过遍历遍历segment对象计数。
3.1 、每一个Segment都各自加锁,那么在调用Size方法的时候,ConcurrentHashMap1.7怎么解决一致性的问题呢?
ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下:
1、遍历所有的Segment。
2、把Segment的元素数量累加起来。
3、把Segment的修改次数累加起来。
4、判断两次是否相同,不同则有修改,重新统计,尝试次数+1;如果不是,说明没有修改,统计结束。如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
5、再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
释放锁,统计结束。
3.2、 为什么这样设计呢?这种思想和乐观锁悲观锁的思想如出一辙。
为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有Segment保证强一致性。
4、ConcurrentHashMap1.8为什么用synchronized,cas不是已经保证操作的线程安全吗?
CAS也是适用一些场合的,比如资源竞争小时,是非常适用的,不用进行内核态和用户态之间 的线程上下文切换,同时自旋概率也会大大减少,提升性能,但资源竞争激烈时(比如大量线 程对同一资源进行写和读操作)并不适用,自旋概率会大大增加,从而浪费CPU资源,降低性能。
5、ConcurrentHashMap1.8使用并发环境下统计count的方法总结
addcount里计算count总值,先去竞争basecount,然后有其他进程就会去竞争value值,竞争不到value值得继续去竞争basecount,如果countcell起初没有就初始化,长度为2,最后把basecount和所有value值加起来返回,然后同时addcount里还有判断是否要扩容得方法。
个人学习笔记,