前段时间准备研究一波Mybatis,代码下载到IDEA,一路Debug走了一遍,头已经绕晕,准备放弃突然看到一些英文
上面写了啥bug 啥的,于是打开链接看了下,大概就是说的computeIfAbsent 方法,如果key存在的情况下也会加锁,会影响性能,后面又百度了下,发现还有其他bug
在写本文前,也看了些网上的文章,大概就是说的是在调用computeIfAbsent(key, …)方法时,正好其他线程需要在key对应位置插入结点,因为computeIfAbsent方法将位置设置了
ReservationNode
,因此putVal方法中那几个判断条件都不会满足,而导致死循环,只要computeIfAbsent结束了,ReservationNode节点都会被替换,此时循环也就结束了
终归揭底都是computeIfAbsent方法的问题,现在听我一一道来
本文只介绍ComputeIfAbsent核心内容,其他具体知识请阅读相应的文章
推荐一篇ConcurrentHashMap文章,个人感觉写得还不错:https://juejin.cn/post/6956468382736580622
absent: 不存在, 意思就是如果不存在就计算
是空
, 说明key 不存在, 那么根据key计算一个value,插入该元素
不为空
,那么对槽中的Node节点加锁进行遍历,判断是否有对应的key存在
链表尾部
或则添加到红黑树
, 返回val该方法在JDK8 中有bug
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
// 计算hash值
int h = spread(key.hashCode());
V val = null;
// 记录链表中的元素数量,如果大于8 就进行树化,转为红黑树
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();
// 数组中i位置是空的
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
// 创建一个站位的节点
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
// 将i位置设置为占位节点
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
// 计算key对应的value
if ((val = mappingFunction.apply(key)) != null)
// 根据得出的value创建一个节点
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 {
boolean added = false;
// 对节点进行加锁,防止其他线程操作
synchronized (f) {
if (tabAt(tab, i) == f) {
// i位置是一个链表,那么遍历这个链表寻找是否有key对应的节点
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek; V ev;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
val = e.val;
break;
}
Node<K,V> pred = e;
// 找到了链表最后,没有发现跟key相同的节点,那么调用函数计算val
if ((e = e.next) == null) {
if ((val = mappingFunction.apply(key)) != null) {
// 标志位,已添加元素
added = true;
// 添加到链表尾部
pred.next = new Node<K,V>(h, key, val, null);
}
break;
}
}
}
// 如果是红黑树,红黑树具体操作比较复杂,这里不在解释
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(h, key, null)) != null)
val = p.val;
// 计算并添加节点
else if ((val = mappingFunction.apply(key)) != null) {
added = true;
t.putTreeVal(h, key, val);
}
}
}
}
// 是否进行树化
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (!added)
return val;
break;
}
}
}
if (val != null)
addCount(1L, binCount);
return val;
}
Present: 存在
如果hash(key) 映射到table数组 中的槽是空
, 说明key 不存在, 直接返回null
如果hash(key) 映射到table数组 中的槽不为空
,那么对槽中的Node节点加锁进行遍历,判断是否有对应的key存在
计算出的vlalue不为null
,那么将新的value替换旧value, 如果为计算结果为null
, 那么将该节点删除,返回新的valuepublic V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
// 判断是否为null
if (key == null || remappingFunction == null)
throw new NullPointerException();
// 获取hash值
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();
// 计算key所对应table中哪一个元素
else if ((f = tabAt(tab, i = (n - 1) & h)) == null)
break;
// 是否在扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 对key锁对应的元素table[i]加锁
synchronized (f) {
// 元素是否被转移
if (tabAt(tab, i) == f) {
// fh >= 0: 说明此时是链表
if (fh >= 0) {
binCount = 1;
// 遍历table[i] 中的元素,寻找key相同的元素
for (Node<K,V> e = f, pred = null;; ++binCount) {
K ek;
// 成立:说明找到key相同的元素
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 计算得出一个新的value
val = remappingFunction.apply(key, e.val);
// 不为null,直接赋值
if (val != null)
e.val = val;
else { // 为null,那么key对应的元素将被删除
delta = -1;
Node<K,V> en = e.next;
if (pred != null)
pred.next = en;
else
setTabAt(tab, i, en);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
// 红黑树中查找节点
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(h, key, null)) != null) {
val = remappingFunction.apply(key, p.val);
if (val != null)
p.val = val;
else {
delta = -1;
if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (binCount != 0)
break;
}
}
// 当调用remappingFunction后计算得出为null时,delta会变为-1,意味着key对应的元素删除,这里将map数量-1
if (delta != 0)
addCount((long)delta, binCount);
// 返回计算得出的值
return val;
}
这里只分析computeIfAbsent代码,computeIfPresent类似
实例代码:
public static void main(String[] args) {
Map<String, Integer> map = new ConcurrentHashMap<>(16);
// 这里会阻塞,因为AaAa的hash跟BBBB相同, 在插入BBBB时会导致for无限循环
map.computeIfAbsent(
"AaAa",
key -> {
return map.computeIfAbsent(
"BBBB",
key2 -> 42);
}
);
此代码来自https://bugs.openjdk.java.net/browse/JDK-8062841
以上面案例来分析BUG: 标号代码见最下面
,建议将代码复制到一边,在读下面的解释
AaAa跟BBBB计算出的hash值都是一样的,因此他们会放在同一个位置
在插入AaAa时,在执行①时,发现数组中没有这个元素,那么创建一个ReservationNode
(hash = -3)节点作为占位,对该节点加synchronized锁,将该节点放在需要插入元素的位置i上(③)
向下执行到③,调用apply计算value,调用上述代码第7行的 map.computeIfAbsent(“BBBB”,
key2 -> 42);将这个结果作为AaAa的value
执行上述7行代码后再次进入computeIfAbsent
方法,此时①不成立,因为在第二步插入了占位符
判断条件⑤(表示的是这个位置是否正在进行扩容), 不成立,进入条件⑥,在⑦获取锁,因为当前线程在第二步已经获取过锁,因为synchronized是可重入
的,继续向下走
进入条件⑧发现不满足,因为ReservationNode的hash为-3,向下执行,发现条件⑨也不满足
进入下一次for循环,往复执行 for语句和 ⑥⑦⑧⑨,导致死循环
由于广大开发者发现了这个bug, 官方也建议过不要使用递归调用,为了让开发者感知这个错误因此在JDK 9中对代码进行了升级
在上面的案例中,因为程序会在⑥⑦⑧⑨来回往复执行,程序bug主要是在⑧⑨判断时都无法满足条件,因此陷入了死循环,JDK 9 在⑨下面新增加了一个条件,代码如下
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
当程序检查出f是一个占位符节点直接抛出异常,以此来阻止死循环
上面的案例是基于待插入元素位置i处为空的情况,如果i位置已经有元素
的话,可能会造成数据丢失的bug, 如下代码:
// 保证初始化空间为16(传入16 会变成32) 才能满足插入的三个元素在长度16的数组中是相同的位置
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(10);
map.put("A0", 1); // 这里会首先放到数组15位置, 另外两个计算结果同样在15位置
// map.put("AaAa", 1);
// map.put("BBBB", 1);
map.computeIfAbsent(
"AaAa",
key -> {
return map.computeIfAbsent(
"BBBB",
key2 -> 42);
}
);
Enumeration<String> keys = map.keys();
while (keys.hasMoreElements()) {
String s = keys.nextElement();
System.out.println(s);
}
运行上述代码会发现BBBB丢失
了
此时运行过程将会变为如下:
首先插入A0到15位置
执行computeIfAbsent,插入AaAa节点(同样在15位置),执行⑦⑧,遍历链表到末尾(pred 记录链表最后一个元素,即A0),执行到⑩继续调用apply计算value
在次调用 map.computeIfAbsent(“BBBB”…. , 将pred.next 赋值为BBBB,即此时A0.next = BBBB,
返回42,回到第一次插入AaAa时
因为第一步的pred记录的是最开始链表最后一个节点A0
,现在在次执行pred.next = AaAa,
此时A0并没有指向第二步插入的BBBB节点了
,BBBB节点就被丢失
这种情况虽然不会死循环
,但是会造成数据的丢失,因此为了防止这种情况发生,在④内部新增了代码:
if ((val = mappingFunction.apply(key)) != null) {
if (pred.next != null)
throw new IllegalStateException("Recursive update");
如果发现pred.next != null, 说明已经被赋值了,如果在次对pred.next赋值,就会造成数据丢失
如下图:
// 建议将代码复制到一边,在读上面的解释
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
int h = spread(key.hashCode());
V val = null;
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if 表为空 对表进行初始化
// ①
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
Node<K,V> r = new ReservationNode<K,V>();
// ②
synchronized (r) {
// ③
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
// ④
if ((val = mappingFunction.apply(key)) != null)
node = new Node<K,V>(h, key, val, null);
} finally {
setTabAt(tab, i, node);
}
}
}
}
// ⑤
else if ((fh = f.hash) == MOVED)
// ⑥
else {
// ⑦
synchronized (f) {
if (tabAt(tab, i) == f) {
// ⑧
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek; V ev;
if (存在key) {
// 将老的value替换为新的value
break;
}
Node<K,V> pred = e;
// ⑩
if ((e = e.next) == null) {
if ((val = mappingFunction.apply(key)) != null) {
pred.next = new Node<K,V>(h, key, val, null);
}
break;
}
}
}
// ⑨
else if (f instanceof TreeBin) {
.... // 跟链表逻辑类似
}
}
}
}
}
return val;
}
本人才疏学浅,文中若有错误,还请指出!