ConcurrentHashMap en Java

La clase Java ConcurrentHashMap forma parte de las Concurrency Collection Classes. Es una implementación de tabla hash que admite la recuperación y actualización concurrente. Se utiliza en un entorno multi-hilo para evitar la ConcurrentModificationException.

ConcurrentHashMap

Si intentamos modificar la colección mientras iteramos sobre ella, obtenemos ConcurrentModificationException. Java 1.5 introdujo clases concurrentes en el paquete java.util.concurrent para superar este escenario. ConcurrentHashMap es la implementación de Map que nos permite modificar el Map durante la iteración. Las operaciones de ConcurrentHashMap son seguras para subprocesos. ConcurrentHashMap no permite nulos como claves y valores.

Ejemplo de Java ConcurrentHashMap

La clase ConcurrentHashMap es similar a HashMap, excepto que es segura para subprocesos y permite la modificación durante la iteración.

package com.journaldev.util;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {

	public static void main(String[] args) {

		//ConcurrentHashMap
		Map<String,String> myMap = new ConcurrentHashMap<String,String>();
		myMap.put("1", "1");
		myMap.put("2", "1");
		myMap.put("3", "1");
		myMap.put("4", "1");
		myMap.put("5", "1");
		myMap.put("6", "1");
		System.out.println("ConcurrentHashMap before iterator: "+myMap);
		Iterator<String> it = myMap.keySet().iterator();

		while(it.hasNext()){
			String key = it.next();
			if(key.equals("3")) myMap.put(key+"new", "new3");
		}
		System.out.println("ConcurrentHashMap after iterator: "+myMap);

		//HashMap
		myMap = new HashMap<String,String>();
		myMap.put("1", "1");
		myMap.put("2", "1");
		myMap.put("3", "1");
		myMap.put("4", "1");
		myMap.put("5", "1");
		myMap.put("6", "1");
		System.out.println("HashMap before iterator: "+myMap);
		Iterator<String> it1 = myMap.keySet().iterator();

		while(it1.hasNext()){
			String key = it1.next();
			if(key.equals("3")) myMap.put(key+"new", "new3");
		}
		System.out.println("HashMap after iterator: "+myMap);
	}

}

Salida:

ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}
ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}
HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
	at java.util.HashMap$KeyIterator.next(HashMap.java:828)
	at com.test.ConcurrentHashMapExample.main(ConcurrentHashMapExample.java:44)

Es evidente a partir del resultado que ConcurrentHashMap se encarga de la nueva entrada en el mapa durante la iteración, mientras que HashMap arroja ConcurrentModificationException. Observemos de cerca la traza de la excepción. La siguiente declaración ha lanzado la excepción.

String key = it1.next();

Significa que la nueva entrada se insertó en HashMap pero el iterador está fallando. De hecho, el iterador en objetos Collection es fail-fast, es decir, cualquier modificación en la estructura o en el número de entradas en el objeto de colección activará la excepción.

¿Cómo sabe el iterador sobre la modificación en la Colección?

Hemos tomado el conjunto de claves de HashMap y luego iteramos sobre él. HashMap contiene una variable para contar el número de modificaciones, y el iterador la utiliza cuando llamas a su función next() para obtener la siguiente entrada. HashMap.java:

/**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient volatile int modCount;

Vamos a cambiar un poco el código para salir del bucle del iterador cuando insertamos la nueva entrada. Todo lo que necesitamos hacer es agregar una declaración de break después de la llamada a put.

if(key.equals("3")){
	myMap.put(key+"new", "new3");
	break;
}

El resultado con el código anterior:

ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}
ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}
HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}
HashMap after iterator: {3=1, 2=1, 1=1, 3new=new3, 6=1, 5=1, 4=1}

¿Qué sucede si se modifica el valor de la clave?

¿Qué pasa si no agregamos una nueva entrada pero actualizamos el par clave-valor existente? ¿Lanzará una excepción? Cambiemos el código en el programa original y veamos.

//myMap.put(key+"new", "new3");
myMap.put(key, "new3");

No habrá ninguna excepción porque la colección se modifica pero su estructura permanece igual.

Lectura adicional

¿Notaste esos corchetes angulares al crear nuestro objeto de colección e Iterator? Se llaman genéricos y son muy poderosos cuando se trata de verificar tipos en tiempo de compilación para eliminar ClassCastException en tiempo de ejecución. Aprende más sobre genéricos en Ejemplo de Genéricos en Java. También deberías leer Preguntas de Entrevista sobre Colecciones en Java y Patrón de Diseño Iterator en Java.

Puedes revisar más ejemplos de colecciones de Java en nuestro Repositorio de GitHub.

Referencia: Documentación de la API

Source:
https://www.digitalocean.com/community/tutorials/concurrenthashmap-in-java