Java架构师交流群:793825326
java版本:jdk1.8
IDE:idea 18
使用java8运行如下代码:
ConcurrentHashMap map=new ConcurrentHashMap<>(16);
map.computeIfAbsent("AaAa", key->map.computeIfAbsent("BBBB",key2->42));
System.out.println("success");
这段代码将不会执行到System.out.println("success");因为上面的操作造成了死循环,如果将“AaAa”或者“BBBB”改成其他的东西,比如cccc,代码就可以正常执行了。
是什么原因导致了这个问题呢,这就涉及到ConcurrentHashMap使用的线程安全策略了。如果对这块不了解的,可以看下我的另外一篇博文https://blog.csdn.net/dap769815768/article/details/96596287
我们跟踪进computeIfAbsent方法内看下为何会卡住:
public V computeIfAbsent(K key, Function mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
V val = null;
int binCount = 0;
for (Node[] tab = table;;) {
Node 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 r = new ReservationNode();
synchronized (r) {
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node node = null;
try {
if ((val = mappingFunction.apply(key)) != null)
node = new Node(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) {
if (fh >= 0) {
binCount = 1;
for (Node 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 pred = e;
if ((e = e.next) == null) {
if ((val = mappingFunction.apply(key)) != null) {
added = true;
pred.next = new Node(h, key, val, null);
}
break;
}
}
}
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin t = (TreeBin)f;
TreeNode 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;
}
在第一次调用该方法的时候,代码会进入到这句:
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
Node r = new ReservationNode();
synchronized (r) {
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node node = null;
try {
if ((val = mappingFunction.apply(key)) != null)
node = new Node(h, key, val, null);
} finally {
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
}
这句执行到
val = mappingFunction.apply(key)
这句,会执行
key->map.computeIfAbsent("BBBB",key2->42)
来获取到值,以便完成最终的数据插入。这就会导致重新进入到computeIfAbsent方法里面,这个时候的table在31索引(”AaAa“和”BBBB“计算出来单的索引都是31)位置上是有值的,它是一个ReservationNode
因此第二次进入到computeIfAbsent方法内,由于synchronized是可重入锁,所以代码并不会阻塞,会执行到下面的代码里面去:
else {
boolean added = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node 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 pred = e;
if ((e = e.next) == null) {
if ((val = mappingFunction.apply(key)) != null) {
added = true;
pred.next = new Node(h, key, val, null);
}
break;
}
}
}
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin t = (TreeBin)f;
TreeNode 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;
}
}
这个循环唯一的推出条件是binCount!=0,这就要求
a)if (fh >= 0)
b)else if (f instanceof TreeBin)
这两个条件至少有一个成立,其中fh = f.hash,普通的节点,如果正在被transfer,也就是扩容操作,hash会被设置为-1,这个时候这个线程检测到hash为-1,会帮忙扩容。但这里的f为占位节点,它的默认hash为-3,这可以根据它的构造方法看到
static final class ReservationNode extends Node {
ReservationNode() {
super(RESERVED, null, null, null);
}
Node find(int h, Object k) {
return null;
}
}
其中
static final int RESERVED = -3;
所以第一个条件不会满足,第二个条件很明显也不会满足,那么这个循环将永远不会结束,于是线程就卡在这里了。
这个问题和锁无关,属于设计问题,由于线程安全使用的策略是CAS,也就是自旋操作,导致必须符合条件,才能正常退出循环,但是恰好因为占位节点的hash值被设计成了-3,所以这个循环无论无何也无法达到退出条件了。
因此,官方也不建议在存入数据的时候嵌入其他的存数据的操作。