使用java8运行如下代码:
ConcurrentHashMap<String,Integer> 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使用的线程安全策略了。
我们跟踪进computeIfAbsent方法内看下为何会卡住:
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) { if (key == null || mappingFunction == null) throw new NullPointerException(); 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 (tab == null || (n = tab.length) == 0) tab = initTable(); 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); } } } 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<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; 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; }
在第一次调用该方法的时候,代码会进入到这句: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); } } } if (binCount != 0) break; }
执行到 val = mappingFunction.apply(key) 其实就是执行 key->map.computeIfAbsent("BBBB",key2->42) 来获取到值,以便完成最终的数据插入。这就会导致重新进入到computeIfAbsent方法里面,这个时候的table在31索引(”AaAa“和”BBBB“计算出来单的索引都是31)位置上是有值的,它是一个ReservationNode<K,V>节点,它的作用相当于一个占位符,并没有其他的作用,它的key和value全为null。
因此第二次进入到computeIfAbsent方法内,由于synchronized是可重入锁,所以代码并不会阻塞,会执行到下面的代码里面去:
else { boolean added = false; synchronized (f) { if (tabAt(tab, i) == f) { 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; 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; } }
这个循环唯一的推出条件是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<K,V> extends Node<K,V> { ReservationNode() { super(RESERVED, null, null, null); } Node<K,V> find(int h, Object k) { return null; } }
其中static final int RESERVED = -3;
所以第一个条件不会满足,第二个条件很明显也不会满足,那么这个循环将永远不会结束,于是线程就卡在这里了。这个问题和锁无关,属于设计问题,由于线程安全使用的策略是CAS,也就是自旋操作,导致必须符合条件,才能正常退出循环,但是恰好因为占位节点的hash值被设计成了-3,所以这个循环无论无何也无法达到退出条件了。
因此,官方也不建议在存入数据的时候嵌入其他的存数据的操作。
java8的ConcurrentHashMap的死循环问题
最新推荐文章于 2023-01-23 15:34:03 发布