¿Es un HashMap seguro para diferentes claves?

87

Si tengo dos subprocesos múltiples accediendo a un HashMap, pero garantizo que nunca accederán a la misma clave al mismo tiempo, ¿podría eso conducir a una condición de carrera?

Helder S Ribeiro
fuente

Respuestas:

99

En la respuesta de @ dotsid, dice esto:

Si cambia un HashMap de alguna manera, su código simplemente está roto.

El esta en lo correcto. Un HashMap que se actualiza sin sincronización se romperá incluso si los subprocesos utilizan conjuntos de claves disjuntos. Estas son algunas de las cosas que pueden salir mal.

  • Si un hilo hace una put, entonces otro hilo puede ver un valor obsoleto para el tamaño del mapa de hash.

  • Cuando un subproceso hace un putque desencadena una reconstrucción de la tabla, otro subproceso puede ver versiones transitorias o obsoletas de la referencia de matriz de tabla hash, su tamaño, su contenido o las cadenas hash. Puede sobrevenir el caos.

  • Cuando un subproceso hace una putclave para una clave que colisiona con alguna clave utilizada por algún otro subproceso, y el último subproceso hace una putclave para su clave, entonces este último puede ver una copia obsoleta de la referencia de la cadena hash. Puede sobrevenir el caos.

  • Cuando un hilo palpa la mesa con una llave que choca con una de las llaves de algún otro hilo, puede encontrar esa llave en la cadena. Llamará a equals en esa clave, y si los subprocesos no están sincronizados, el método equals puede encontrar un estado obsoleto en esa clave.

Y si tiene dos hilos haciendo puto removesolicitando simultáneamente , hay numerosas oportunidades para las condiciones de carrera.

Puedo pensar en tres soluciones:

  1. Utilice un ConcurrentHashMap.
  2. Utilice un sistema regular HashMappero sincronizado en el exterior; por ejemplo, usando mutex primitivos, Lockobjetos, etcétera.
  3. Use uno diferente HashMappara cada hilo. Si los subprocesos realmente tienen un conjunto de claves disjunto, entonces no debería ser necesario (desde una perspectiva algorítmica) que compartan un solo mapa. De hecho, si sus algoritmos involucran a los subprocesos que iteran las claves, valores o entradas del mapa en algún momento, dividir el mapa único en múltiples mapas podría dar una aceleración significativa para esa parte del procesamiento.
Esteban C
fuente
30

Simplemente use un ConcurrentHashMap. El ConcurrentHashMap usa múltiples bloqueos que cubren un rango de cubos de hash para reducir las posibilidades de que un bloqueo sea impugnado. Existe un impacto marginal en el rendimiento al adquirir un candado sin oposición.

Para responder a su pregunta original: De acuerdo con el javadoc, mientras la estructura del mapa no cambie, está bien. Esto significa que no se eliminan elementos en absoluto y no se agregan nuevas claves que aún no están en el mapa. Reemplazar el valor asociado con las claves existentes está bien.

Si varios subprocesos acceden a un mapa hash al mismo tiempo, y al menos uno de los subprocesos modifica el mapa estructuralmente, debe sincronizarse externamente. (Una modificación estructural es cualquier operación que agrega o elimina una o más asignaciones; simplemente cambiar el valor asociado con una clave que una instancia ya contiene no es una modificación estructural).

Aunque no ofrece garantías sobre la visibilidad. Por lo tanto, debe estar dispuesto a aceptar la recuperación de asociaciones obsoletas de vez en cuando.

Tim Bender
fuente
6

Depende de lo que quieras decir con "acceder". Si solo está leyendo, puede leer incluso las mismas claves siempre que la visibilidad de los datos esté garantizada bajo las reglas de " sucede antes ". Esto significa que HashMapno debería cambiar y todos los cambios (construcciones iniciales) deberían completarse antes de que cualquier lector comience a acceder HashMap.

Si cambia HashMapde alguna manera, su código simplemente está roto. @ Stephen C proporciona una muy buena explicación de por qué.

EDITAR: Si el primer caso es su situación real, le recomiendo que lo use Collections.unmodifiableMap()para asegurarse de que su HashMap nunca se cambie. Los objetos señalados por HashMapno deben cambiar también, por lo que el uso agresivo de finalpalabras clave puede ayudarlo.

Y como dice @Lars Andren, ConcurrentHashMapes la mejor opción en la mayoría de los casos.

Denis Bazhenov
fuente
2
ConcurrentHashMap es la mejor opción en mi opinión. La única razón por la que no lo recomendé, porque el autor no lo preguntó :) Tiene menos rendimiento debido a las operaciones CAS, pero como dice la regla de oro de la programación concurrente: "Hágalo bien y solo entonces hágalo rápido ":)
Denis Bazhenov
unmodifiableMapasegura que el cliente no pueda cambiar el mapa. No hace nada para garantizar que el mapa subyacente no se cambie.
Pete Kirkham
Como ya señalé: "Los objetos que son señalados por HashMap no deberían cambiar también"
Denis Bazhenov
4

La modificación de un HashMap sin la sincronización adecuada de dos subprocesos puede conducir fácilmente a una condición de carrera.

  • Cuando a put()conduce a un cambio de tamaño de la tabla interna, esto lleva algún tiempo y el otro subproceso continúa escribiendo en la tabla anterior.
  • Dos put()para diferentes claves conducen a una actualización del mismo depósito si los códigos hash de las claves son iguales en módulo al tamaño de la tabla. (En realidad, la relación entre el código hash y el índice de depósito es más complicada, pero aún pueden producirse colisiones).
Christian Semrau
fuente
1
Es peor que las condiciones de carrera. Dependiendo de las partes internas de la HashMapimplementación que esté utilizando, puede dañar las HashMapestructuras de datos, etcétera, causadas por anomalías en la memoria.
Stephen C