主要区别如下:
(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赋给局部变量以保证读到的是当前的最新值,且保证逻辑
版权归原作者 一个射手座的程序媛 所有, 如有侵权,请联系我们删除。