前几天和拼多多及政采云的架构师们闲聊,其中拼多多架构师说遇到了一个ConcurrentHashMap死循环问题,当时内心想这不科学呀?ConcurrentHashMap怎么还有死循环呢,毕竟它已经解决HashMap中rehash中死循环问题了,可是随着深刻的分析,发现事情并无以前想的那么简单~ (如下分析基于jdk版本:jdk1.8.0_171)java
保险起见,不能直接贴出出现问题的业务代码,所以将该问题简化成以下代码:node
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();// map默认capacity 16,当元素个数达到(capacity - capacity >> 2) = 12个时会触发rehashfor (int i = 0; i < 11; i++) { map.put(i, i);}
map.computeIfAbsent(12, (k) -> { // 这里会致使死循环 :( map.put(100, 100); return k;});
// 其余操做
感兴趣的小伙伴能够在电脑上运行下,话不说多,先说下问题缘由:当执行computeIfAbsent
时,若是key对应的slot为空,此时会建立ReservationNode
对象(hash值为RESERVED=-3
)放到当前slot位置,而后调用mappingFunction.apply(key)
生成value,根据value建立Node以后赋值到slow位置,此时完成computeIfAbsent
流程。可是上述代码mappingFunction
中又对该map进行了一次put操做,而且触发了rehash操做,在transfer
中遍历slot数组时,依次判断slot对应Node是否为null、hash值是否为MOVED=-一、hash值否大于0(list结构)、Node类型是不是TreeBin(红黑树结构),惟独没有判断hash值为RESERVED=-3
的状况,所以致使了死循环问题。数组
问题分析到这里,缘由已经很清楚了,当时咱们认为,这多是jdk的“bug”
,所以咱们最后给出的解决方案是:安全
1.若是在rehash时出现了slot
节点类型是ReservationNode
,能够给个提示,好比抛异常;2.理论上来讲,mappingFunction
中不该该再对当前map进行更新操做了,可是jdk并无禁止不能这样用,最好说明下。微信
最后,另外一个朋友看了computeIfAbsent
的注释:架构
/** * If the specified key is not already associated with a value, * attempts to compute its value using the given mapping function * and enters it into this map unless {@code null}. The entire * method invocation is performed atomically, so the function is * applied at most once per key. Some attempted update operations * on this map by other threads may be blocked while computation * is in progress, so the computation should be short and simple, * and must not attempt to update any other mappings of this map. */public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
咱们发现,其实人家已经知道了这个问题,还特地注释说明了。。。咱们仍是too yong too simple
啊。至此,ConcurrentHashMap死循环问题告一段落,仍是要遵循编码规范,不要在mappingFunction
中再对当前map进行更新操做。其实ConcurrentHashMap死循环不只仅出如今上述讨论的场景中,如下场景也会触发,缘由和上述讨论的是同样的,代码以下,感兴趣的小伙伴也能够本地跑下:并发
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();map.computeIfAbsent(12, (k) -> { map.put(k, k); return k;});
System.out.println(map);// 其余操做
最后,一块儿跟着computeIfAbsent源码来分下上述死循环代码的执行流程,限于篇幅,只分析下主要流程代码:app
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) { // 这里使用synchronized针对局部对象意义不大,主要是下面的cas操做保证并发问题 if (casTabAt(tab, i, null, r)) { binCount = 1; Node<K,V> node = null; try { // 这里的value返回可能为null呦 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) { // 仅仅判断了node.hash >=0和node为TreeBin类型状况,未判断`ReservationNode`类型 // 扩容时判断和此处相似 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;}
好文推荐:less
以为文章不错,请点右下方“在看”并把咱们推荐给身边朋友。欢迎小伙伴关注【TopCoder】阅读更多精彩好文。
本文分享自微信公众号 - TopCoder(gh_12e4a74a5c9c)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。