Saltar a contenido

Guía de Decisión: Colecciones Concurrentes en Java

Esta guía te ayuda a elegir la colección concurrente adecuada para tu caso de uso.


📊 Tabla Comparativa Rápida

Colección Tipo Ordenada Bloqueante Mejor para
ArrayList + synchronized Lista No N/A Legacy, evitar si es posible
CopyOnWriteArrayList Lista No No Lecturas frecuentes, pocas escrituras
CopyOnWriteArraySet Set No No Lecturas frecuentes, sin duplicados
ConcurrentHashMap Mapa No No Mapas de alto rendimiento (default choice)
ConcurrentSkipListMap Mapa No Mapas ordenados concurrentes
ConcurrentSkipListSet Set No Sets ordenados sin duplicados
ConcurrentLinkedQueue Cola FIFO No Colas FIFO no bloqueantes
ArrayBlockingQueue Cola FIFO Productor/consumidor con límite
LinkedBlockingQueue Cola FIFO Productor/consumidor sin límite definido

🌳 Árbol de Decisión

¿Necesitas una lista (índice/posición)?
├── SÍ → ¿Muchas lecturas y pocas escrituras?
│   ├── SÍ → CopyOnWriteArrayList
│   └── NO → Reconsidera (¿Quizás un Mapa?)
└── NO → ¿Necesitas una cola (FIFO)?
    ├── SÍ → ¿Necesitas bloquear cuando esté llena/vacía?
    │   ├── SÍ → ArrayBlockingQueue (con límite) o LinkedBlockingQueue
    │   └── NO → ConcurrentLinkedQueue
    └── NO → ¿Necesitas un Set (sin duplicados)?
        ├── SÍ → ¿Necesitas ordenación natural?
        │   ├── SÍ → ConcurrentSkipListSet
        │   └── NO → CopyOnWriteArraySet
        └── NO → ¿Necesitas un Mapa (clave-valor)?
            ├── SÍ → ¿Necesitas ordenación por clave?
            │   ├── SÍ → ConcurrentSkipListMap
            │   └── NO → ConcurrentHashMap (default)
            └── NO → Considera colecciones especializadas o Atomic variables

📈 Por Categoría de Uso

Lectura Intensiva + Escrituras Esporádicas

Mejor opción: CopyOnWriteArrayList / CopyOnWriteArraySet

  • ✅ Sin bloqueos en lectura (snapshot iteration)
  • ✅ Thread-safe por diseño
  • ❌ Costoso en escritura (copia completa)
  • 🎯 Ideal: Configuraciones, listeners, suscriptores

Mapa de Alto Rendimiento General

Mejor opción: ConcurrentHashMap

  • ✅ Lock por segmento (no bloqueo global)
  • ✅ Operaciones atómicas: putIfAbsent, compute, merge
  • ✅ Excelente throughput concurrente
  • ❌ No ordenado
  • 🎯 Default choice para mapas concurrentes

Mapa Ordenado Concurrente

Mejor opción: ConcurrentSkipListMap

  • ✅ Ordenación por clave (Comparable o Comparator)
  • ✅ O(log n) operaciones
  • ✅ Submapas concurrentes (headMap, tailMap)
  • ❌ Más overhead que CHM
  • 🎯 Rangos, ordenación, navegación

Cola No Bloqueante

Mejor opción: ConcurrentLinkedQueue

  • ✅ Lock-free (algoritmo de Michael-Scott)
  • ✅ No bloquea threads
  • ✅ Weakly consistent iterator
  • ❌ No limita tamaño
  • ❌ No soporta null
  • 🎯 Trabajos en background, pipelines

Productor/Consumidor

Mejor opción: ArrayBlockingQueue (con límite) o LinkedBlockingQueue

  • ✅ Back-pressure integrado
  • ✅ put() / take() bloqueantes
  • ✅ Timeout configurable
  • 🎯 Pipelines, thread pools, batch processing

Contadores Concurrentes

Mejor opción: LongAdder (contadores) / DoubleAdder

  • ✅ Stripe de contadores (reduce CAS contention)
  • ✅ Mucho mejor throughput que AtomicLong bajo alta contención
  • ❌ Mayor consumo de memoria
  • ❌ sum() puede no reflejar valor exacto en momento de lectura
  • 🎯 Métricas, estadísticas, contadores de alta frecuencia

⚠️ Cuándo NO Usar

Colección NO usar cuando... Alternativa
CopyOnWriteArrayList Escrituras frecuentes Collections.synchronizedList o reconsidera diseño
ConcurrentHashMap Necesitas ordenación ConcurrentSkipListMap
ConcurrentLinkedQueue Necesitas tamaño exacto frecuentemente LinkedBlockingQueue
ArrayBlockingQueue No conoces capacidad máxima LinkedBlockingQueue
LongAdder Necesitas lectura consistente siempre AtomicLong
Cualquier colección concurrente Acceso single-thread Colección no concurrente (mejor rendimiento)

🔢 Complejidad Temporal

Operación CHM CSLM CLQ ABQ
get O(1) O(log n) O(n)* O(1)
put/add O(1) O(log n) O(1) O(1)
remove O(1) O(log n) O(1) O(1)
contains O(1) O(log n) O(n) O(n)
size() O(n) O(1) O(n) O(1)

*CLQ: búsqueda es O(n) pero inserción/espera es O(1) con operaciones CAS


💡 Patrones Comunes

1. Caché Concurrente

ConcurrentHashMap<Key, Value> cache = new ConcurrentHashMap<>();

// ComputeIfAbsent es atómico
Value value = cache.computeIfAbsent(key, k -> loadFromDatabase(k));

2. Contadores de Frecuencia

ConcurrentHashMap<String, LongAdder> frequencies = new ConcurrentHashMap<>();

// Incremento thread-safe
frequencies.computeIfAbsent(word, k -> new LongAdder()).increment();

3. Work Queue

BlockingQueue<WorkItem> queue = new ArrayBlockingQueue<>(1000);

// Productor
queue.put(item); // Bloquea si llena

// Consumidor
WorkItem item = queue.take(); // Bloquea si vacía

4. Event Listeners

CopyOnWriteArrayList<EventListener> listeners = new CopyOnWriteArrayList<>();

// Registro (copia interna)
listeners.add(listener);

// Notificación (itera snapshot sin bloqueos)
for (EventListener l : listeners) {
    l.onEvent(event);
}

🎯 Checklist de Decisión

Antes de elegir una colección, responde:

  • [ ] ¿Es concurrente el acceso? (Si no, usa HashMap/ArrayList normales)
  • [ ] ¿Qué operaciones predominan? (lectura/escritura/mezcla)
  • [ ] ¿Necesitas ordenación?
  • [ ] ¿Necesitas tamaño limitado?
  • [ ] ¿Necesitas bloqueo cuando vacío/lleno?
  • [ ] ¿Puedes tolerar iteradores "weakly consistent"?
  • [ ] ¿Es crítico el uso de memoria?

📚 Referencias


Documento generado para thread-safe-collections v1.0.0