7 errores críticos que todos cometen con la gestión de hilos en Java

java 7 errores críticos que todos cometen con la gestión de hilos en Java

7 errores críticos que todos cometen con la gestión de hilos en Java

La concurrencia es una de las áreas donde incluso desarrolladores experimentados cometen fallos que luego cuestan rendimiento, estabilidad o seguridad. Aquí tienes 7 errores frecuentes, ejemplos mínimos reproducibles y la forma correcta de resolverlos.

1) No sincronizar el acceso a datos compartidos (race condition)

Ejemplo: contador incrementado desde múltiples hilos sin protección.

public class RaceDemo {
    static int counter = 0;

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) counter++;
            });
            threads[i].start();
        }
        for (Thread t : threads) t.join();
        System.out.println("counter=" + counter); // valor inesperado
    }
}

Solución: usar AtomicInteger, o sincronización/locks cuando haga falta:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) counter.incrementAndGet();
            });
            threads[i].start();
        }
        for (Thread t : threads) t.join();
        System.out.println("counter=" + counter.get()); // esperable
    }
}

2) Usar wait/notify sin mientras (while) - pérdida de señal y spurious wakeups

Error común: usar if en vez de while, lo que puede aceptar condiciones falsas tras un wakeup.

// Mal
synchronized (lock) {
    if (!ready) lock.wait();
    // continuar
}

// Bien
synchronized (lock) {
    while (!ready) lock.wait();
    // continuar solo cuando ready == true
}

Además, cuando hay múltiples consumidores/proveedores usa notifyAll() para evitar hilos que quedan bloqueados.

3) Deadlocks por orden de bloqueo inconsistente

Ejemplo mínimo (dos hilos, dos bloqueos):

Object a = new Object();
Object b = new Object();

// Thread 1
synchronized (a) {
    Thread.sleep(50);
    synchronized (b) { }
}

// Thread 2
synchronized (b) {
    Thread.sleep(50);
    synchronized (a) { }
}

Solución: definir un orden global para adquirir locks o usar tryLock con timeout (ReentrantLock).

ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
        if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
            try { /* trabajo */ } finally { lockB.unlock(); }
        }
    } finally { lockA.unlock(); }
}

4) No cerrar/terminar pools de hilos (leaks)

Crear un ExecutorService y olvidarse de cerrarlo deja hilos vivos que impiden la salida y consumen recursos.

ExecutorService ex = Executors.newFixedThreadPool(4);
// submit tareas...
// olvidamos shutdown()

// Correcto:
ex.shutdown();
if (!ex.awaitTermination(10, TimeUnit.SECONDS)) {
    ex.shutdownNow();
}

Si usas frameworks, delega la gestión de lifecycle al contenedor o usa try-with-resources con wrappers cuando sea posible.

5) Creer que volatile sustituye a la sincronización

volatile garantiza visibilidad de lecturas/escrituras en variables individuales, pero no atomicidad para operaciones compuestas (por ejemplo: x++).

volatile int x;
// x++ NO es atómico: lectura + incremento + escritura

Usa AtomicInteger o sincronización para operaciones compuestas o invariantes relacionadas.

6) Coordinar con Thread.sleep() en lugar de herramientas de sincronización

Thread.sleep() es impreciso y frágil para coordinación entre hilos (timings, carga de CPU, etc.). Usa CountDownLatch, Semaphore, CyclicBarrier o Phaser.

CountDownLatch latch = new CountDownLatch(1);
// productor
latch.countDown();
// consumidor
latch.await();

7) Bloquear recursos largos dentro de secciones sincronizadas

Bloquear y luego realizar operaciones de E/S o llamadas remotas dentro de un bloque sincronizado puede reducir la concurrencia drásticamente.

// Mal: sección sincronizada incluye I/O
synchronized (cacheLock) {
    if (!cache.containsKey(key)) {
        String data = remoteCall(); // lento
        cache.put(key, data);
    }
}

// Mejor: doble-checked locking o computeIfAbsent con ConcurrentHashMap
String data = cache.get(key);
if (data == null) {
    data = remoteCall();
    cache.putIfAbsent(key, data);
}

Herramientas y prácticas para depurar y evitar errores

  • jstack / jcmd para obtener stacks y detectar deadlocks
  • Java Flight Recorder / Mission Control para perfiles en producción
  • VisualVM para inspeccionar hilos y heap
  • Usar concurrencia de alto nivel: ExecutorService, CompletableFuture, concurrent collections
  • Escribir tests deterministas: concurrencystress, jcstress o pruebas con contadores/fences

Patrones y buenas prácticas rápidas

  • Preferir colecciones concurrentes (ConcurrentHashMap) a sincronización manual cuando sea posible
  • Minimizar la sección crítica: mantener sincronización lo más corta posible
  • Documentar el orden de adquisición de locks en el código/arquitectura
  • Favor abstraer la concurrencia en componentes reutilizables y probados

Consejo avanzado: si tu aplicación puede migrar a versiones recientes de la JVM, evalúa usar virtual threads (Project Loom) y structured concurrency: simplifican enormemente muchos de los patrones de concurrencia tradicionales, pero no eximen de entender sincronización, límites de recursos y operaciones bloqueantes. Como siguiente paso, prueba convertir un módulo con muchas callbacks a CompletableFuture o a virtual threads para comparar complejidad y rendimiento.

Advertencia: siempre perfila cambios concurrentes en condiciones de carga reales; lo que funciona en microbenchmarks puede fallar en producción debido a latencias y contención.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión