JDK8 ConcurrentHashMap.computeIfAbsent 带来的问题

死循环问题

JDK8中的ConcurrentHashMap也不一定是安全的。
官方Bug报告: https://bugs.openjdk.java.net/browse/JDK-8062841
JDK9中变化内容: http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/6dd59c01f011
参考demo

package at.irian.misc.javabug;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> map = new ConcurrentHashMap<>(16);
        map.computeIfAbsent(
                "AaAa",
                key -> {
                    return map.computeIfAbsent(
                            "BBBB",
                            key2 -> 42);
                }
        );
    }
}

问题在第一次调用AaAa时,会创建ReservationNode节点保存在map中
第二次调用时,因为节点存在,而且是synchronized 可重入,会调用else后代码
ReservationNode hash=-3 if elseif 都不会进入
会导致循环调用

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;
}

MyBatis 3.5.X遇上JDK8出现了性能问题

主要是在并发调用ConcurrentHashMap.computeIfAbsent方法时,为保证原子性,对相同的key进行修改时,会造成线程阻塞。

MyBatis 3.5.4不存在该问题,因为那是还没引进computeIfAbsent这个函数
参考DefaultReflectorFactory.java

  @Override
  public Reflector findForClass(Class<?> type) {
    if (classCacheEnabled) {
            // synchronized (type) removed see issue #461
      Reflector cached = reflectorMap.get(type);
      if (cached == null) {
        cached = new Reflector(type);
        reflectorMap.put(type, cached);
      }
      return cached;
    } else {
      return new Reflector(type);
    }
  }

目前MyBatis 3.5.7已修复该问题
引进了一个工具类,临时修复了JDK-8161372这个issue
JDK-8161372 issue中有并发测试computeIfAbsent的demo

public class MapUtil {
  /**
   * A temporary workaround for Java 8 specific performance issue JDK-8161372 .<br>
   * This class should be removed once we drop Java 8 support.
   *
   * @see <a href="https://bugs.openjdk.java.net/browse/JDK-8161372">https://bugs.openjdk.java.net/browse/JDK-8161372</a>
   */
  public static <K, V> V computeIfAbsent(Map<K, V> map, K key, Function<K, V> mappingFunction) {
    V value = map.get(key);
    if (value != null) {
      return value;
    }
    return map.computeIfAbsent(key, mappingFunction::apply);
  }

  /**
   * Map.entry(key, value) alternative for Java 8.
   */
  public static <K, V> Entry<K, V> entry(K key, V value) {
    return new AbstractMap.SimpleImmutableEntry<>(key, value);
  }

  private MapUtil() {
    super();
  }
}

参考

  1. 不一样的内容:死磕JDK8中ConcurrentHashMap.computeIfAbsent 死循环 Bug
  2. 我慌了!MyBatis 3.5.X遇上JDK8竟然出现了性能问题,全项目组都得加班~
  3. ConcurrentHashMap.computeIfAbsent stuck in an endless loop
  4. ConcurrentHashMap.computeIfAbsent(k,f) locks bin when k present
  5. java8的ConcurrentHashMap的死循环问题
posted @ 2021-04-27 12:11  zendwang  阅读(674)  评论(0编辑  收藏  举报