Error de interbloqueo de JDK8 ConcurrentHashMap

En JDK1.8, su implementación interna ha cambiado mucho. El par interno ya no usa la versión 1.7 del bloqueo de segmento, pero usa sincronizado + CAS (clase insegura) para lograr bloqueos exclusivos de grano fino más eficientes para cada nodo en el mapa Actualización.

En la nueva implementación, el código de operación de actualización para elementos ha cambiado mucho. Por ejemplo, el uso de los siguientes métodos provocará un interbloqueo si no tiene cuidado .

public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
       ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(16);
       map.computeIfAbsent(
           "AaAa",
           key ->  map.computeIfAbsent("BBBB", key2 -> 42)
       );

Citado de: https://stackoverflow.com/questions/43861945/deadlock-in-concurrenthashmap

La ejecución del fragmento de código anterior provocará un interbloqueo. Cuando key = "AaAa" no existe en el mapa, la clave computeIfAbsentse insertará y el valor de retorno (42) de la siguiente función lamda se utilizará como su valor. Y esta función lamda continuará realizando la misma operación en el nodo con la tecla = "BBBB" y el valor establecido = 42. Sin embargo, debido a que el código hash de la cadena "AaAa" y "BBBB" aquí es el mismo, el punto muerto de ejecución (https://stackoverflow.com/questions/43861945/deadlock-in-concurrenthashmap).

   key ->  map.computeIfAbsent("BBBB", key2 -> 42);

Este artículo https://www.jianshu.com/p/59bd27e137e1 cree que es causado por la operación CAS en el método computeIfAbsent. Sincronizado es un bloqueo reentrante. Obtener el bloqueo del mismo Nodo dos veces no bloqueará. Por qué CAS causará ¿este problema? Yo también estoy en desacuerdo con su afirmación y se necesita un análisis más detallado.

Probé el bloque de código anterior en una clase llamada ConcurrentMapBug y
jstack -l pidobtuve el contenido de la pila de ejecución del hilo a través de los siguientes comandos :

"main" #1 prio=5 os_prio=0 tid=0x0062e000 nid=0x614 runnable [0x005ef000]
   java.lang.Thread.State: RUNNABLE
        at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1718)
        at concurrent.map.ConcurrentMapBug.lambda$main$1(ConcurrentMapBug.java:13)
        at concurrent.map.ConcurrentMapBug$$Lambda$1/10634667.apply(Unknown Source)
        at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660)
        - locked <0x049a9c60> (a java.util.concurrent.ConcurrentHashMap$ReservationNode)
        at concurrent.map.ConcurrentMapBug.main(ConcurrentMapBug.java:11)

Note estas líneas:

java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660)
        - locked <0x049a9c60> (a java.util.concurrent.ConcurrentHashMap$ReservationNode)
        at concurrent.map.ConcurrentMapBug.main(ConcurrentMapBug.java:11)

Se dice que estaba bloqueado en la línea 1660 en el código JDK. Verifique el código fuente de ConcurrentHashMap:

     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; //JDK1.8.0_152源代码中1648行,binCount值初始化为0 
        for (Node<K,V>[] tab = table;;) {
    
     #1649行
            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) {
    
     //1653行
            //(n - 1) & h计算出key在tab中的存放位置i
                Node<K,V> r = new ReservationNode<K,V>(); //1654行,新建一个占位Node r
                synchronized (r) {
    
     //1655行,获取Node r的monitor锁
                    if (casTabAt(tab, i, null, r)) {
    
     //1656行
                    //通过CAS方式将Node r插入到map内置的table中
                        binCount = 1;
                        Node<K,V> node = null;
                        try {
    
    
                            if ((val = mappingFunction.apply(key)) != null) //1660行
                                node = new Node<K,V>(h, key, val, null);
                        } finally {
    
    
                            setTabAt(tab, i, node);
                        }
                    }
                }
                if (binCount != 0)
                    break;
            }
            else if ((fh = f.hash) == MOVED) //MOVED值为-1
                 tab = helpTransfer(tab, f);
            else {
    
     //1672行
                boolean added = false;
                synchronized (f) {
    
     //f为1654行插入的ReservationNode,
                //monitor对同一个线程可重入,内层mappingFunction.apply到这里时不会阻塞
                    if (tabAt(tab, i) == f) {
    
    
                        if (fh >= 0) {
    
     //1676行,ReservationNode的hash为-3
                            binCount = 1; //上面例子不会被执行到这里
                            ....
                        }
                        else if (f instanceof TreeBin) {
    
    //TreeBin Node的hash为-2
                           ....
                        }
                    }
                }
                if (binCount != 0) {
    
     //1710行,
                //内层mappingFunction.apply执行时在1648行初始化binCount=0,
                //第二次调用computeIfAbsent时不会执行到if body里面代码
                   if (binCount >= TREEIFY_THRESHOLD)
                      treeifyBin(tab, i);
                   if (!added)
                      return val;
                   break;
           } //end 1672行else
        } //end 1649行的for_loop
       ...
    }//end of computeIfAbsent

map.computeIfAbsent("AaAa", mapFunction)Ingresará al ramal de la línea 1653. En la línea 1654 del código, se creará aquí un Nodo de marcador de posición ( hash = -3 , clave = nulo, valor = nulo, siguiente Nodo = nulo), y luego el Nodo se insertará en el mapa (línea 1656). Cada subproceso que ejecuta este método puede crear dicho nodo por sí mismo, por lo que varios subprocesos pueden operar la misma clave al mismo tiempo, pero solo un subproceso puede tener éxito en la operación CAS, que es diferente del método exclusivo de la versión anterior 1.7.

En la línea 1656 del código, CAS es un bloqueo de giro. No hay adquisición de bloqueo. En general, la eficiencia es mayor. Aquí solo se ejecutará una vez. Si la ejecución es exitosa, la ejecución continuará. El éxito aquí significa que no hay otros hilos escribiendo en este momento. La misma clave para el mapa.

Cuando la ejecución alcance la línea 1660, se ejecutará key -> map.computeIfAbsent("BBBB", key2 -> 42);y se volverá a llamar aquí computeIfAbsent. Debido a que el hash de "AaAa" y "BBBB" son iguales, el valor se almacenará en la misma ubicación, por lo que cuando la ejecución alcance línea 1653, se obtiene el marcador de posición previamente insertado Nodo f. , note = f.hash = -3 FH , se ejecutan en el ramal línea 1672, se determina la línea 1676 fh> = 0 es falso, por lo tanto el final del ramal, ya que el código para retroceder ya no sale de la línea de bucle 1649, por lo que ingresará al siguiente ciclo y repetirá el proceso anterior. Por lo tanto, el código se ejecuta repetidamente en el bucle de la línea 1649 en lugar de bloquearse.

En resumen, map.computeIfAbsent("AaAa", mapFunction)mientras espera el valor de retorno de mapFunction.apply (key), mapFunction: key -> map.computeIfAbsent("BBBB", key2 -> 42);ha entrado en un bucle sin fin y nunca regresará. Entonces, todo el código debe ejecutarse y bloquearse, pero ¿es esto un punto muerto ? ¡Parece ser diferente de la definición de punto muerto!

En resumen, para evitar este problema, al usar ConcurrentHashMap en JDK1.8, no realice la operación de actualizar el valor de otros nodos en la función lambda de computeIfAbsent.

Este punto se menciona realmente en el documento de Java del método:

Algunas operaciones de actualización intentadas en este mapa por otros subprocesos pueden bloquearse mientras el cálculo está en progreso, por lo que el cálculo debe ser breve y simple, y no debe intentar actualizar ninguna otra asignación de este mapa.

En resumen, no actualice otros elementos en el mapa en una operación de actualización como en el ejemplo anterior.

Supongo que te gusta

Origin blog.csdn.net/lx1848/article/details/81256443
Recomendado
Clasificación