Rendimiento ConcurrentHashmap vs HashMap

¿Cómo se compara el rendimiento de ConcurrentHashMap con HashMap, especialmente con la operación .get ()? Estoy especialmente interesado en el caso de pocos artículos, en el rango entre quizás 0-5000.

¿Hay alguna razón para no usar ConcurrentHashMap en lugar de HashMap?

(Sé que los valores nulos no están permitidos)

Actualizar

solo para aclarar, obviamente, el rendimiento en caso de acceso concurrente real sufrirá, pero ¿cómo se compara el rendimiento en caso de que no haya acceso simultáneo?

Me sorprendió mucho encontrar este tema tan viejo y, sin embargo, nadie me ha proporcionado ninguna prueba sobre el caso. Usando ScalaMeter , he creado pruebas de add , get y remove tanto para HashMap como para ConcurrentHashMap en dos escenarios:

  1. utilizando un solo hilo
  2. usando tantos hilos como tengo núcleos disponibles. Tenga en cuenta que debido a que HashMap no es seguro para subprocesos, simplemente creé HashMap por separado para cada subproceso, pero usé uno, ConcurrentHashMap compartido.

El código está disponible en mi repository .

Los resultados son los siguientes:

  • El eje X (tamaño) presenta el número de elementos escritos en el mapa (s)
  • El eje Y (valor) presenta el tiempo en milisegundos

Agregar método Obtener método Eliminar método

El resumen

  • Si desea operar sus datos lo más rápido posible, use todos los hilos disponibles. Eso parece obvio, cada hilo tiene 1/13 del trabajo completo por hacer.

  • Si elige un solo acceso a subprocesos, use HashMap , simplemente es más rápido. Para el método add , es incluso más eficiente que 3 veces. Solo get es más rápido en ConcurrentHashMap , pero no mucho.

  • Cuando se opera en ConcurrentHashMap con muchos subprocesos, es igualmente efectivo operar en HashMaps separados para cada subproceso. Por lo tanto, no es necesario dividir sus datos en diferentes estructuras.

En resumen, el rendimiento de ConcurrentHashMap es peor cuando se utiliza con un solo hilo, pero agregar más hilos para hacer el trabajo acelerará definitivamente el proceso.

Plataforma de prueba

AMD FX6100, 16GB Ram
Xubuntu 16.04, Oracle JDK 8 actualización 91, Scala 2.11.8

La seguridad de subprocesos es una pregunta compleja. Si desea que un objeto sea seguro, hágalo conscientemente y documente esa elección. Las personas que usan tu clase te lo agradecerán si es seguro para la ejecución de subprocesos cuando simplifica su uso, pero te maldecirán si un objeto que una vez fue seguro para subprocesos no lo es en una versión futura. La seguridad del hilo, aunque es realmente agradable, ¡no es solo para Navidad!

Entonces ahora a tu pregunta:

ConcurrentHashMap (al menos en la implementación actual de Sun ) funciona dividiendo el mapa subyacente en una cantidad de segmentos diferentes. Obtener un elemento no requiere ningún locking per se, pero sí utiliza operaciones atómicas / volátiles, lo que implica una barrera de memoria (potencialmente muy costosa e interfiere con otras optimizaciones posibles).

Incluso si todos los gastos generales de las operaciones atómicas pueden ser eliminados por el comstackdor de JIT en un caso de subproceso único, aún existe la sobrecarga de decidir cuál de los cubos debe mirar, sin embargo, este es un cálculo relativamente rápido, pero no obstante, es imposible de eliminar

En cuanto a decidir qué implementación usar, la elección es probablemente simple.

Si se trata de un campo estático, es casi seguro que desee utilizar ConcurrentHashMap, a menos que las pruebas muestren que este es un verdadero asesino de rendimiento. Su clase tiene expectativas de seguridad de hilos diferentes de las instancias de esa clase.

Si se trata de una variable local, entonces es probable que un HashMap sea suficiente, a menos que sepa que las referencias al objeto pueden filtrarse a otro hilo. Al codificar en la interfaz de Mapa, te permites cambiarlo fácilmente más tarde si descubres un problema.

Si se trata de un campo de instancia, y la clase no se ha diseñado para que sea segura para subprocesos, documentarla como no segura para subprocesos y usar un HashMap.

Si sabe que este campo de instancia es la única razón por la cual la clase no es segura para subprocesos y está dispuesta a vivir con las restricciones que implica la prometedora seguridad de subprocesos, entonces use ConcurrentHashMap, a menos que las pruebas muestren implicaciones de rendimiento significativas. En ese caso, podría considerar permitir que un usuario de la clase elija de algún modo una versión segura para subprocesos del hilo, quizás utilizando un método de fábrica diferente.

En cualquier caso, documente la clase como segura para subprocesos (o con seguridad de subprocesos) para que las personas que usan su clase sepan que pueden usar objetos en varios subprocesos, y las personas que editan su clase saben que deben mantener la seguridad de subprocesos en el futuro.

Le recomendaría que lo mida, ya que (por una razón) puede haber cierta dependencia de la distribución hash de los objetos particulares que está almacenando.

El hashmap estándar no proporciona protección de concurrencia, mientras que el hashmap concurrente sí lo hace. Antes de que estuviera disponible, podías ajustar el hashmap para obtener acceso seguro a las hebras pero esto era un locking de grano grueso y significaba que todos los accesos simultáneos se serializaban, lo que realmente podría afectar el rendimiento.

El hashmap concurrente usa locking de locking y solo bloquea elementos afectados por un locking particular. Si está ejecutando en un vm moderno, como hotspot, la vm intentará utilizar el locking de sesgo, el desplazamiento y ellision si es posible, por lo que solo pagará la penalización de los lockings cuando realmente lo necesite.

En resumen, si su mapa va a ser accesado por hilos concurrentes y necesita garantizar una vista consistente de su estado, use el hashmap concurrente.

En el caso de una tabla hash de 1000 elementos que usa 10 lockings para la tabla completa, se ahorra casi la mitad del tiempo cuando 10000 hilos se están insertando y 10000 hilos se están borrando de ella.

La interesante diferencia de tiempo de ejecución está aquí

Siempre use la estructura de datos concurrente. excepto cuando la desventaja de las rayas (mencionado a continuación) se convierte en una operación frecuente. En ese caso, tendrá que adquirir todos los lockings? Leí que la mejor manera de hacerlo es recurriendo.

Las divisiones de locking son útiles cuando hay una forma de romper un locking de contención alto en lockings múltiples sin comprometer la integridad de los datos. Si esto es posible o no, debe pensar un poco y no siempre es el caso. La estructura de datos también es el factor que contribuye a la decisión. Entonces, si usamos una matriz grande para implementar una tabla hash, usar un único locking para toda la tabla hash para sincronizarla conducirá a que los hilos accedan secuencialmente a la estructura de datos. Si esta es la misma ubicación en la tabla hash, entonces es necesario pero, ¿y si están accediendo a los dos extremos de la tabla?

El lado negativo de las franjas de locking es que es difícil obtener el estado de la estructura de datos que se ve afectada por las bandas. En el ejemplo, el tamaño de la tabla, o tratar de enumerar / enumerar toda la tabla puede ser engorroso, ya que necesitamos adquirir todos los lockings seccionados.

¿Qué respuesta esperas aquí?

Obviamente dependerá de la cantidad de lecturas que se realicen al mismo tiempo que las escrituras y de cuánto tiempo debe “bloquearse” un mapa normal en una operación de escritura en su aplicación (y si se usaría el método putIfAbsent en ConcurrentMap ) . Cualquier punto de referencia va a ser en gran medida sin sentido.

No está claro cuál es tu significado. Si necesita seguridad de subprocesos, casi no tiene elección, solo ConcurrentHashMap. Y definitivamente tiene penalizaciones de rendimiento / memoria en la llamada get (): acceso a variables volátiles y locking si no tiene suerte.