Concurrent Map
Concurrent Map
✅ Introducción
En aplicaciones concurrentes de Java, donde múltiples subprocesos acceden y modifican una misma estructura de datos de manera simultánea, es fundamental garantizar la consistencia de los datos y la seguridad de los subprocesos (thread safety).
ConcurrentMap<K, V> es una interfaz de Java que representa una colección de pares clave-valor que pueden ser accedidos y modificados simultáneamente por múltiples hilos sin necesidad de sincronización explícita por parte del desarrollador.
Extiende la interfaz estándar Map<K, V>, proporcionando operaciones adicionales para facilitar el acceso y modificación segura y eficiente en entornos concurrentes.
🎯 ¿Por qué utilizar ConcurrentMap?
- ✅ Evita condiciones de carrera y asegura la consistencia de los datos sin necesidad de usar
synchronizedoLocksexplícitos. - ✅ Mejora el rendimiento en aplicaciones multihilo, gracias a su enfoque de sincronización a nivel granular.
- ✅ Proporciona operaciones atómicas útiles como
putIfAbsent,remove,replace, y más.
🏗️ Principales implementaciones de ConcurrentMap
| Implementación | Descripción breve |
|---|---|
ConcurrentHashMap<K, V> |
Implementación más común; ofrece alta eficiencia y bajo bloqueo. |
ConcurrentSkipListMap<K, V> |
Mapa concurrente ordenado, basado en skip lists. |
ConcurrentNavigableMap<K, V> |
Subinterfaz de ConcurrentMap, añade navegación de claves ordenadas. |
ConcurrentLinkedHashMap<K, V> (*) |
Implementación de terceros que mantiene el orden de inserción. |
(*) Nota: ConcurrentLinkedHashMap no es parte del JDK estándar, pero es popular en cachés de terceros como Caffeine.
🔖 La más utilizada y recomendada para la mayoría de los casos es ConcurrentHashMap, por su excelente rendimiento y facilidad de uso.
🧰 Métodos clave en ConcurrentMap
V putIfAbsent(K key, V value)- Inserta un valor solo si la clave no está presente.
boolean remove(Object key, Object value)- Elimina un valor asociado solo si coincide con el especificado.
boolean replace(K key, V oldValue, V newValue)- Reemplaza el valor solo si coincide con el valor actual.
V replace(K key, V value)- Reemplaza el valor si la clave está presente.
💡 Ejemplo práctico: Cache concurrente
Implementaremos un caché concurrente que simula cómo varios hilos escriben y leen de un ConcurrentHashMap de forma segura.
Código de ejemplo:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentCacheDemo {
// Estructura del caché compartido
private static ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
public static void main(String[] args) {
// Creamos 10 hilos que interactúan con el caché
for (int i = 0; i < 10; i++) {
int threadNum = i;
Thread thread = new Thread(() -> {
String key = "key-" + threadNum;
// Intentamos obtener el valor tres veces
for (int j = 0; j < 3; j++) {
String value = getValue(key);
System.out.printf("Thread %s fetched: Key = %s | Value = %s%n",
Thread.currentThread().getName(), key, value);
}
});
thread.start();
}
}
private static String getValue(String key) {
// Intentamos obtener el valor del caché
String value = cache.get(key);
if (value == null) {
// Si no existe, lo calculamos y lo insertamos en el caché
value = computeValue(key);
cache.put(key, value);
}
return value;
}
private static String computeValue(String key) {
System.out.printf("Key %s not found in cache. Computing value...%n", key);
try {
Thread.sleep(500); // Simula operación costosa
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Value for " + key;
}
}
🔎 Funcionamiento interno de ConcurrentHashMap
ConcurrentHashMap es la implementación más eficiente de ConcurrentMap. Internamente utiliza una segmentación y bloqueos a nivel de segmento para maximizar el rendimiento en entornos concurrentes.
1. Adición de elementos
Pasos internos:
- Hash y determinación del segmento
- El hash de la clave se calcula para determinar a qué segmento pertenece.
- Bloqueo del segmento
- El hilo adquiere el lock del segmento para garantizar seguridad en la inserción.
- Inserción en el segmento
- Se inserta el par clave-valor en la tabla del segmento.
- Liberación del bloqueo
- Se libera el lock para permitir que otros hilos accedan al segmento.
2. Obtención de elementos
Pasos internos:
- Hash y determinación del segmento
- Acceso seguro sin bloqueo completo
- Lecturas concurrentes pueden realizarse sin bloqueo completo, gracias a la naturaleza de la estructura.
- Devolución del valor
- Si el valor existe, se devuelve; si no, se indica la ausencia.
⚠️ Desde Java 8, ConcurrentHashMap ha mejorado su implementación eliminando segmentos y usando una estructura de nodos y tablas junto con CAS (Compare-And-Swap) para reducir el bloqueo.
🚀 Ventajas de ConcurrentHashMap sobre Collections.synchronizedMap()
| Característica | ConcurrentHashMap | synchronizedMap |
|---|---|---|
| Granularidad del bloqueo | A nivel de segmento o sin bloqueo en lecturas | Bloqueo de todo el mapa |
| Lecturas concurrentes sin bloqueo | Sí | No |
| Rendimiento en entornos altamente concurrentes | Alto | Bajo |
Operaciones atómicas específicas (putIfAbsent, etc.) |
Sí | No (requiere sincronización manual) |
⚙️ Buenas prácticas
- Usar
putIfAbsent()para evitar condiciones de carrera al inicializar valores. - Evitar el uso de
nullcomo clave o valor;ConcurrentHashMapno permitenull. - Preferir
ConcurrentHashMapsobreHashtableoCollections.synchronizedMap().
📚 Conclusiones
ConcurrentMapes clave en aplicaciones multihilo, seguras y eficientes.ConcurrentHashMapes la implementación predilecta por su simplicidad y rendimiento.- Facilita el desarrollo concurrente sin bloqueos explícitos y con operaciones atómicas.
💡 En la mayoría de los casos de uso donde se requiere acceso concurrente a un Map, ConcurrentHashMap es suficiente.