ConcurrentHashMap atrapado en un bucle infinito - ¿Por qué?

Bufón :

Mientras se hace un análisis a fondo de ConcurrentHashMap, encontrado un blog en internet que dice que incluso ConcurrentHashMappueden quedar atrapados en un bucle infinito.

Se da este ejemplo. Cuando me encontré con este código - se quedó atascado:

public class Test {
    public static void main(String[] args) throws Exception {
        Map<Long, Long> map = new ConcurrentHashMap<>();
        map.put(0L, 0L);
        map.put((1L << 32) + 1, 0L);
        for (long key : map.keySet()) {
            map.put(key, map.remove(key));
        }
    }
}

Por favor, explique por qué sucede este punto muerto.

Marco13:

Como ya se ha dicho: No es un callejón sin salida, pero un bucle infinito. Independientemente de ello, el núcleo (y título) de la pregunta es: ¿Por qué sucede esto?

Las otras respuestas no entrar en mucho detalle aquí, pero tenía curiosidad para comprender mejor este también. Por ejemplo, cuando se cambia la línea

map.put((1L << 32) + 1, 0L);

a

map.put(1L, 0L);

entonces no , no se atascan. Y de nuevo, la pregunta es por qué .


La respuesta es: Es complicado.

El ConcurrentHashMapes una de las clases más complejas desde el marco de las colecciones / concurrente, con la friolera de 6300 líneas de código, con 230 líneas de comentarios que explican la única básico concepto de la aplicación, y por qué la magia y el código ilegible en realidad funciona . La siguiente es bastante simplificado, pero al menos debe explicar el básico problema.

En primer lugar: El conjunto que devuelve Map::keySetes una vista sobre el estado interno. Y el JavaDoc dice:

Devuelve una vista de conjunto de las claves contenidas en este mapa. El conjunto está respaldado por el mapa, por lo que los cambios en el mapa se reflejan en el conjunto, y viceversa. Si el mapa se modifica mientras una iteración sobre el conjunto está en curso (excepto a través propia operación remove del iterador), los resultados de la iteración no están definidos. La eliminación de soportes conjunto de elementos, [...]

(El subrayado por mí)

Sin embargo, el JavaDoc de ConcurrentHashMap::keySetdice:

Devuelve una vista de conjunto de las claves contenidas en este mapa. El conjunto está respaldado por el mapa, por lo que los cambios en el mapa se reflejan en el conjunto, y viceversa. La eliminación de soportes conjunto de elementos, [...]

(Tenga en cuenta que lo hace no mencionar el comportamiento indefinido!)

Por lo general, la modificación del mapa, mientras que la iteración en la keySetlanzaría una ConcurrentModificationException. Pero el ConcurrentHashMapes capaz de hacer frente a esto. Se mantiene constante y aún puede repetirse otra vez, a pesar de que los resultados todavía pueden ser inesperados - como en su caso.


Al llegar a la razón de la conducta que observó:

Una tabla hash (o mapa hash) funciona básicamente mediante el cálculo de un valor hash de la clave, y el uso de esta clave como un indicador para el "cubo" que la entrada debe añadirse a. Cuando varias teclas se asignan a la misma cubeta, a continuación, las entradas en el cubo suelen ser gestionados como una lista enlazada . Lo mismo es el caso de la ConcurrentHashMap.

El siguiente programa utiliza algunos hacks reflexión desagradables para imprimir el estado interno de la tabla - en particular, los "cubos" de la tabla, que consta de nodos - durante la iteración y la modificación:

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MapLoop
{
    public static void main(String[] args) throws Exception
    {
        runTestInfinite();
        runTestFinite();
    }

    private static void runTestInfinite() throws Exception
    {
        System.out.println("Running test with inifinite loop");

        Map<Long, Long> map = new ConcurrentHashMap<>();
        map.put(0L, 0L);
        map.put((1L << 32) + 1, 0L);

        int counter = 0;
        for (long key : map.keySet())
        {
            map.put(key, map.remove(key));

            System.out.println("Infinite, counter is "+counter);
            printTable(map);

            counter++;
            if (counter == 10)
            {
                System.out.println("Bailing out...");
                break;
            }
        }

        System.out.println("Running test with inifinite loop DONE");
    }

    private static void runTestFinite() throws Exception
    {
        System.out.println("Running test with finite loop");

        Map<Long, Long> map = new ConcurrentHashMap<>();
        map.put(0L, 0L);
        map.put(1L, 0L);

        int counter = 0;
        for (long key : map.keySet())
        {
            map.put(key, map.remove(key));

            System.out.println("Finite, counter is "+counter);
            printTable(map);

            counter++;
        }

        System.out.println("Running test with finite loop DONE");
    }


    private static void printTable(Map<Long, Long> map) throws Exception
    {
        // Hack, to illustrate the issue here:
        System.out.println("Table now: ");
        Field fTable = ConcurrentHashMap.class.getDeclaredField("table");
        fTable.setAccessible(true);
        Object t = fTable.get(map);
        int n = Array.getLength(t);
        for (int i = 0; i < n; i++)
        {
            Object node = Array.get(t, i);
            printNode(i, node);
        }
    }

    private static void printNode(int index, Object node) throws Exception
    {
        if (node == null)
        {
            System.out.println("at " + index + ": null");
            return;
        }
        // Hack, to illustrate the issue here:
        Class<?> c =
            Class.forName("java.util.concurrent.ConcurrentHashMap$Node");
        Field fHash = c.getDeclaredField("hash");
        fHash.setAccessible(true);
        Field fKey = c.getDeclaredField("key");
        fKey.setAccessible(true);
        Field fVal = c.getDeclaredField("val");
        fVal.setAccessible(true);
        Field fNext = c.getDeclaredField("next");
        fNext.setAccessible(true);

        System.out.println("  at " + index + ":");
        System.out.println("    hash " + fHash.getInt(node));
        System.out.println("    key  " + fKey.get(node));
        System.out.println("    val  " + fVal.get(node));
        System.out.println("    next " + fNext.get(node));
    }
}

La salida para el runTestInfinitecaso es el siguiente (partes redundantes omitidas):

Running test with infinite loop
Infinite, counter is 0
Table now: 
  at 0:
    hash 0
    key  4294967297
    val  0
    next 0=0
at 1: null
at 2: null
...
at 14: null
at 15: null
Infinite, counter is 1
Table now: 
  at 0:
    hash 0
    key  0
    val  0
    next 4294967297=0
at 1: null
at 2: null
...
at 14: null
at 15: null
Infinite, counter is 2
Table now: 
  at 0:
    hash 0
    key  4294967297
    val  0
    next 0=0
at 1: null
at 2: null
...
at 14: null
at 15: null
Infinite, counter is 3
...
Infinite, counter is 9
...
Bailing out...
Running test with infinite loop DONE

Se puede observar que las entradas para la llave 0y la llave 4294967297(que es su (1L << 32) + 1) siempre terminan en el cubo 0, y se mantiene como una lista enlazada. Por lo que la iteración sobre las keySetaperturas con esta tabla:

Bucket   :   Contents
   0     :   0 --> 4294967297
   1     :   null
  ...    :   ...
  15     :   null

En la primera iteración, se retira la llave 0, básicamente girar la mesa en éste:

Bucket   :   Contents
   0     :   4294967297
   1     :   null
  ...    :   ...
  15     :   null

Pero la clave 0se añade inmediatamente después, y termina en el mismo cubo como 4294967297- por lo que se añade al final de la lista:

Bucket   :   Contents
   0     :   4294967297 -> 0
   1     :   null
  ...    :   ...
  15     :   null

(Esto se indica por la next 0=0parte de la salida).

En la siguiente iteración, el 4294967297se retira y se reinserta, con lo que la tabla en el mismo estado que tenía inicialmente.

Y ahí es donde el bucle infinito viene.


En contraste con esto, la salida para el runTestFinitecaso es lo siguiente:

Running test with finite loop
Finite, counter is 0
Table now: 
  at 0:
    hash 0
    key  0
    val  0
    next null
  at 1:
    hash 1
    key  1
    val  0
    next null
at 2: null
...
at 14: null
at 15: null
Finite, counter is 1
Table now: 
  at 0:
    hash 0
    key  0
    val  0
    next null
  at 1:
    hash 1
    key  1
    val  0
    next null
at 2: null
...
at 14: null
at 15: null
Running test with finite loop DONE

Se puede observar que las teclas 0y 1terminar en diferentes cubos. Así que hay lista a la que se podrían anexan los elementos retirados (y añadidos) no ligadas, y las termina el bucle después de iteración a través de los elementos pertinentes (es decir, los primeros dos cubos) una vez .

Supongo que te gusta

Origin http://43.154.161.224:23101/article/api/json?id=225982&siteId=1
Recomendado
Clasificación