Pregunta ¿ConcurrentMap.remove () proporciona una ventaja antes de que get () devuelva null?


Son acciones en un hilo antes de llamar ConcurrentMap.remove() garantizado suceder antes acciones posteriores a ver la eliminación de otro hilo?

Documentación Dice esto con respecto a los objetos colocados en la colección:

Las acciones en un subproceso antes de colocar un objeto en cualquier colección simultánea ocurren antes que las acciones posteriores al acceso o eliminación de ese elemento de la colección en otro subproceso.

Código de ejemplo:

{
    final ConcurrentMap map = new ConcurrentHashMap();
    map.put(1, new Object());

    final int[] value = { 0 };

    new Thread(() -> {
        value[0]++;
        value[0]++;
        value[0]++;
        value[0]++;
        value[0]++;

        map.remove(1); // A

    }).start();

    new Thread(() -> {
        if (map.get(1) == null) { // B
            System.out.println(value[0]); // expect 5
        }

    }).start();
}

Es UN en un pasa-antes una relación con segundo? Por lo tanto, ¿debería el programa solo, si alguna vez, imprimir 5?


5
2017-09-06 06:12


origen


Respuestas:


Ha encontrado un aspecto sutil interesante de estas herramientas de simultaneidad que es fácil pasar por alto.

En primer lugar, es imposible proporcionar una garantía general sobre la eliminación y la recuperación de un null referencia, ya que este último solo prueba la ausencia de un mapeo pero no un anterior eliminación, es decir, el hilo podría haber leído el estado inicial del mapa, antes de que la clave tuviera un mapeo, lo que, por supuesto, no puede establecer una pasa-antes Relación con las acciones que ocurrieron luego de la construcción del mapa.

Además, si hay varios subprocesos que eliminan la misma clave, no puede suponer una pasa-antes relación, al recuperar null, ya que no sabes qué eliminación se ha completado. Este problema es similar al escenario cuando dos subprocesos insertan el mismo valor, pero este último puede solucionarse en el lado de la aplicación realizando solo inserciones de valores distinguibles o siguiendo el patrón habitual de realizar las modificaciones deseadas en el objeto de valor que se está ejecutando. para ser insertado y para consultar el objeto recuperado solamente. Para una eliminación, no hay tal solución.

En tu caso especial, hay un pasa-antes relación entre el map.put(1, new Object()) acción y el inicio del segundo hilo, por lo que si el segundo hilo se encuentra null al consultar la clave 1Está claro que fue testigo de la eliminación exclusiva de su código. Sin embargo, la especificación no se molestó en proporcionar una garantía explícita para este caso especial.

En cambio, la especificación de Java 8 ConcurrentHashMap dice,

Las recuperaciones reflejan los resultados de la última terminado operaciones de actualización que se mantienen en su inicio. (Más formalmente, una operación de actualización para una clave dada lleva un pasa-antes relación con cualquier recuperación (no nula) para esa clave que informa el valor actualizado.)

descartando claramente null recuperaciones

Creo que, con la actual (Java 8) ConcurrentHashMap implementación, su código no se puede romper ya que es bastante conservador ya que realiza todo el acceso a su matriz de respaldo interna con volatile semántica. Pero eso es sólo el corriente implementación y, como se explicó anteriormente, su código es un caso especial y es probable que se rompa con cada cambio hacia una aplicación de la vida real.


4
2017-09-06 17:00



No, tienes el orden equivocado.

Hay un pasa-antes borde de la put() a la subsiguiente get(). Ese borde no es simétrico y no funciona en la otra dirección. No hay pasa-antes borde de en get() a otro get() o una remove(), o de un put() a otro put().

En este caso, pones un objeto en el mapa. Luego modificas otro objeto. Eso es un no-no. No hay ninguna ventaja de los que escribe a la get() en el segundo hilo, por lo que esas escrituras pueden no ser visibles para el segundo hilo.

En el hardware de Intel, creo que esto siempre funcionará. Sin embargo, el modelo de memoria de Java no lo garantiza, por lo que debe tener cuidado si alguna vez transfiere este código a un hardware diferente.


1
2017-09-06 06:51



A no tiene que suceder antes de B.

Solo el original put sucede antes que los dos. Por lo tanto, un nulo en B significa que A sucedió.

Sin embargo, escriba de nuevo el caché de memoria local de subproceso y el orden de instrucción de ++ y remove no se mencionan volatile no se utiliza; en su lugar, un mapa y una matriz se utilizan para mantener sincronizados los datos de subprocesos. Al volver a escribir los datos, la relación de orden debería mantenerse nuevamente.

A mi entender, A podría eliminarse y volver a escribirse, luego sucedió el último ++ y se imprimió algo como 4 en B. Yo agregaría volatile a la matriz. El Mapa en sí irá bien.

No estoy seguro, pero como no vi la respuesta correspondiente, saco el cuello. (Para aprender yo mismo.)


1
2017-09-06 08:32



Como ConcurrentHashMap es una colección segura para hilos, la declaración map.remove(1) debe tener una barrera de lectura y una barrera de escritura si altera el mapa. La expresion map.get(1) debe tener una barrera de lectura o una, o ambas operaciones no son seguras para subprocesos.

En realidad, ConcurrentHashMap hasta Java 7, utiliza bloqueos particionados, por lo que siempre tiene una barrera de lectura / escritura para casi todas las operaciones.

Un ConcurrentSkipListMap no tiene que usar bloqueos, pero para realizar cualquier acción de escritura segura para subprocesos, se requiere una barrera de escritura.

Esto significa que su prueba siempre debe actuar como se espera.


1
2017-09-06 06:49



Preguntas populares