11 - Errores comunes en concurrencia Java

Los problemas de concurrencia suelen ser silenciosos y difíciles de depurar. Evita estos errores habituales antes de que lleguen a producción.

11.1 Sincronización insuficiente

  • Race conditions: usar colecciones normales (ArrayList, HashMap) sin protección en escritura concurrente.
  • Lecturas inconsistentes: falta de visibilidad (sin synchronized o atomics) deja estados viejos en caché de CPU.
  • Volatilidad: olvidar volatile en flags de control ocasiona bucles infinitos.
  • Solución: usar colecciones concurrentes, atomics o secciones críticas claras; marcar flags compartidas como volatile.

11.2 Sincronización excesiva

  • Locks globales: sincronizar en un objeto compartido gigante serializa toda la aplicación.
  • Caída de rendimiento: contención alta, colas de espera y CPU ociosa.
  • Invariantes mal delimitadas: sección crítica más grande de lo necesario.
  • Solución: reducir el alcance del lock, dividir por sharding o usar locks más finos y lecturas con ReadWriteLock.

11.3 Deadlocks

  • Orden de locks: adquirir recursos en distinto orden entre hilos provoca interbloqueo.
  • join() mal usado: hilos que se esperan mutuamente sin condición de salida.
  • Solución: orden total de locks, timeouts con tryLock, y revisión de dependencias.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockDemo {
  private static final Lock A = new ReentrantLock();
  private static final Lock B = new ReentrantLock();

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> bloquear(A, B), "t1");
    Thread t2 = new Thread(() -> bloquear(B, A), "t2"); // orden inverso
    t1.start();
    t2.start();
    t1.join(1000);
    t2.join(1000);
    System.out.println("Posible deadlock? t1 vivo=" + t1.isAlive() + " t2 vivo=" + t2.isAlive());
  }

  private static void bloquear(Lock primero, Lock segundo) {
    primero.lock();
    try {
      dormir();
      segundo.lock();
      try {
        System.out.println(Thread.currentThread().getName() + " avanzo");
      } finally {
        segundo.unlock();
      }
    } finally {
      primero.unlock();
    }
  }

  private static void dormir() {
    try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
  }
}

Los hilos toman locks en orden distinto y quedan esperando indefinidamente. Para evitarlo define un orden fijo o usa tryLock con timeout para abandonar si no se obtiene el segundo lock.

11.4 Pools mal configurados

  • Pocos hilos: generan cuellos de botella y latencia alta.
  • Demasiados hilos: overhead de contexto y starvation si bloquean entre sí.
  • No cerrar pools: olvidarse de shutdown() deja hilos vivos y fuga recursos.
  • Solución: dimensionar según cores y tipo de carga (CPU-bound vs I/O-bound), medir y ajustar, y cerrar pools al terminar.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class PoolMalo {
  public static void main(String[] args) throws Exception {
    ExecutorService pool = Executors.newFixedThreadPool(1); // muy chico para 3 tareas lentas
    for (int i = 1; i <= 3; i++) {
      int idx = i;
      pool.submit(() -> {
        System.out.println("Tarea " + idx + " inicia");
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        System.out.println("Tarea " + idx + " fin");
      });
    }
    pool.shutdown();
  }
}

Un pool de 1 hilo obliga a ejecutar todo en serie. Ajusta el tamaño al hardware o usa hilos virtuales para cargas I/O-bound.

11.5 Futuros bloqueantes

  • get() abusivo: bloquea el hilo llamante y puede agotar el pool si se hace dentro de tareas.
  • Sin timeout: riesgo de bloqueo eterno ante fallas externas.
  • Bloqueos cruzados: tareas en el mismo pool que esperan a otras del mismo pool pueden generar starvation.
  • Solución: usar timeouts, CompletableFuture no bloqueante o composición asíncrona; separar pools o usar hilos virtuales para esperas I/O.
import java.util.concurrent.*;

public class FuturoBloqueante {
  public static void main(String[] args) throws Exception {
    ExecutorService pool = Executors.newFixedThreadPool(2);

    Future<String> lento = pool.submit(() -> {
      TimeUnit.SECONDS.sleep(5);
      return "listo";
    });

    try {
      System.out.println("Esperando con timeout...");
      String valor = lento.get(1, TimeUnit.SECONDS);
      System.out.println("Recibido: " + valor);
    } catch (TimeoutException e) {
      System.out.println("Timeout: reintentar o cancelar");
      lento.cancel(true);
    } finally {
      pool.shutdownNow();
    }
  }
}

Siempre aplica timeout al esperar resultados externos. Si el tiempo límite expira, decide si reintentar, cancelar o degradar el servicio para evitar hilos colgados.