Java ConcurrentHashMap 高并发安全实现原理剖析(2),2024年最新真是经典中
重要区别如下:(1)computeIfAbsent只会在判定到key不存在时才会插入,判空与插入是一个原子操作,提供的FunctionalInterface是一个二元的Function, 接受key参数,返回value效果;如果盘算效果为null则不做插入。
(2)computeIfPresent只会在判读单到Key非空时才会做更新,判定非空与插入是一个原子操作,提供的FunctionalInterface是一个三元的BiFunction,接受key,value两个参数,返回新的value效果;如果新的value为null则删除key对应节点。
(3)compute则不加key是否存在的限定,提供的FunctionalInterface是一个三元的BiFunction,接受key,value两个参数,返回新的value效果;如果旧的value不存在则以null替代进行盘算;如果新的value为null则包管key对应节点不会存在。
(4)merge不加key是否存在的限定,提供的FunctionalInterface是一个三元的BiFunction,接受oldValue, newVALUE两个参数,返回merge后的value;如果旧的value不存在,直接以newVALUE作为终极效果,存在则返回merge后的效果;如果终极效果为null,则包管key对应节点不会存在。
2、何时会利用ReserveNode占位
========================
如果目的bin的头节点为null,需要写入的话有两种本领:一种是天生好新的节点r后利用casTabAt(tab, i, null, r)原子操作,因为compare的值为null可以包管并发的安全;
另外一种方式是创建一个占位的ReserveNode,锁住该节点并将其CAS设置到bin的头节点,再进行进一步的原子盘算操作;这两种办法都有大概在CAS的时候失败,需要本身反复尝试。
(1)为什么只有computeIfAbsent/compute方法利用占位符的方式
=============================================
computeIfPresent只有在BIN布局非空的情况下才会展开原子盘算,自然不存在需要ReserveNode占位的情况;锁住已有的头节点即可。
computeIfAbsent/compute方法在BIN布局为空时,需要展开Function大概BiFunction的运算,这个操作是外部引入的需要耗时多久无法准确评估;这种情况下如果采用先盘算,再casTabAt(tab, i, null, r)的方式,如果有其它线程提前更新了这个BIN,那么就需要重新锁定新加入的头节点,并重复一次原子盘算(C13Map无法帮你缓存上次盘算的效果,因为盘算的入参有大概会变化),这个开销是比较大的。
而利用ReserveNode占位的方式无需等到原子盘算出效果,可以第一时间先抢占BIN的所有权,使其他并发的写线程阻塞。
(2)merge方法为何不需要占位
=====================
原因是如果BIN布局为空时,根据merge的处置惩罚策略,老的value为空则直接利用新的value替代,如许就省去了BiFunction中新老value进行merge的盘算,这个斲丧几乎是没有的;因此可以利用casTabAt(tab, i, null, r)的方式直接修改,制止了利用ReserveNode占位,锁定该占位ReserveNode后再进行CAS修改的两次CAS无谓的开销。
C13Map的compute方法
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (key == null || remappingFunction == null)
throw new nullPointerException();
int h = spread(key.hashCode());
V val = null;
int delta = 0;
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) & h)) == null) {
//创建占位Node
Node<K, V> r = new ReservationNode<K, V>();
//先锁定该占位Node
synchronized ® {
//将其设置到BIN的头节点
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K, V> node = null;
try {
//开始原子盘算
if ((val = remappingFunction.apply(key, null)) != null) {
delta = 1;
node = new Node<K, V>(h, key, val, null);
}
} finally {
//设置盘算后的终极节点
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
} else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
//此处省略对普通链表的变更操作
} else if (f instanceof TreeBin) {
//此处省略对红黑树的变更操作
}
}
}
}
}
if (delta != 0)
addCount((long) delta, binCount);
return val;
}
3、如何包管原子性
=============
computeIfAbsent/computeIfPresent中判空与盘算是原子操作,根据上述分析重要是通过casTabAt(tab, i, null, r)原子操作,大概利用ReserveNode占位并锁定的方式,大概锁住bin的头节点的方式来实现的。
也就是说整个bin不停处于锁定状态,在获取到目的KEY的value是否为空以后,其它线程无法变更目的KEY的值,判空与盘算自然是原子的。
而casTabAt(tab, i, null, r)是由硬件层面的原子指令来包管的,能够包管同一个内存区域在compare和set操作之间不会有任何其它指令对其进行变更。
八、resize过程中的并发transfer
==========================
C13Map中总共有三处地方会触发transfer方法的调用,分别是addCount、tryPresize、helpTransfer三个函数。
[*] addCount 用于写操作完成后检验元素数目,如果凌驾了sizeCtl中的阈值,则触发resize扩容和旧表向新表的transfer。
[*] tryPresize 是putAll一次性插入一个集合前的自检,如果集合数目较大,则预先触发一次resize扩容和旧表向新表的transfer。
[*] helpTransfer 是写操作过程中发现bin的头节点是ForwardingNode, 则调用helpTransfer加入帮忙搬运的行列。
1、开始transfer前的检查工作
======================
以addCount中的检查逻辑为例:
addCount中的transfer检查
Node<K, V>[] tab, nt;
int n, sc;
//当前的tableSize已经凌驾sizeCtl阈值,且小于最大值
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))
//尚未搬运,当前线程是本次resize工作的第一个线程,设置初始值为2,非常巧妙的计划
transfer(tab, null);
s = sumCount();
}
多处应用了对变量sizeCtl的CAS操作,sizeCtl是一个全局控制变量。
参考下此变量的定义:private transient volatile int sizeCtl;
[*] 初始值是0表示哈希表尚未初始化
[*] 如果是-1表示正在初始化,只允许一个线程进入初始化代码块
[*] 初始化大概reSize成功后,sizeCtl=loadFactor * tableSize也就是触发再次扩容的阈值,是一个正整数
[*] 在扩容过程中,sizeCtrl是一个负整数,其高16位是与当前的tableSize关联的邮戳resizeStamp,其低16位是当前从事搬运工作的线程数加1
在方法的循环体中每次都将table、sizeCtrl、nextTable赋给局部变量以包管读
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]