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 synchronized o Locks explí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:

  1. Hash y determinación del segmento
    • El hash de la clave se calcula para determinar a qué segmento pertenece.
  2. Bloqueo del segmento
    • El hilo adquiere el lock del segmento para garantizar seguridad en la inserción.
  3. Inserción en el segmento
    • Se inserta el par clave-valor en la tabla del segmento.
  4. Liberación del bloqueo
    • Se libera el lock para permitir que otros hilos accedan al segmento.

2. Obtención de elementos

Pasos internos:

  1. Hash y determinación del segmento
  2. Acceso seguro sin bloqueo completo
    • Lecturas concurrentes pueden realizarse sin bloqueo completo, gracias a la naturaleza de la estructura.
  3. 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 No
Rendimiento en entornos altamente concurrentes Alto Bajo
Operaciones atómicas específicas (putIfAbsent, etc.) No (requiere sincronización manual)

⚙️ Buenas prácticas

  • Usar putIfAbsent() para evitar condiciones de carrera al inicializar valores.
  • Evitar el uso de null como clave o valor; ConcurrentHashMap no permite null.
  • Preferir ConcurrentHashMap sobre Hashtable o Collections.synchronizedMap().

📚 Conclusiones

  • ConcurrentMap es clave en aplicaciones multihilo, seguras y eficientes.
  • ConcurrentHashMap es 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.


🔗 Recursos adicionales