1. Olvidar unlock/release: una trampa para los despistados
Uno de los errores más traicioneros al usar herramientas modernas de sincronización, como ReentrantLock o Semaphore, es olvidar llamar a unlock() o release(). Si no liberas el bloqueo, los demás hilos esperarán su liberación... para siempre. El programa se quedará colgado, y te quedarás mirando la pantalla tratando de entender por qué no pasa nada.
Veamos un ejemplo con ReentrantLock:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
// ¡Ups! Olvidamos unlock() — ahora todos se quedarán bloqueados
count++;
}
}
Todo parece inocente, pero si llamas a increment() varias veces desde distintos hilos, tras la primera llamada los demás hilos esperarán la liberación del bloqueo indefinidamente.
Para evitar esta situación, utiliza la construcción try-finally:
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
Ahora, incluso si en medio del método se produce una excepción, la liberación del bloqueo está garantizada.
Es como si alguien ocupara el baño (se encerrara por dentro) y luego olvidara abrir la puerta y saliera por la ventana. Los demás estarán esperando a que esa persona salga... ¡No lo hagas!
2. Sincronización en el objeto incorrecto: «¡Vaya, puse el candado donde no era!»
En Java, la palabra clave synchronized puede bloquear el acceso a un objeto. Pero si eliges un objeto incorrecto para bloquear, la sincronización no funcionará como esperas.
Error n.º 1: sincronizar sobre una variable local
public void doSomething() {
Object lock = new Object();
synchronized (lock) {
// Cada vez es un objeto nuevo — no hay sincronización.
// Los hilos no se esperan entre sí.
// ¡La sección crítica no está protegida!
}
}
Aquí cada hilo crea su propio objeto lock. Como resultado, no hay bloqueo real: los hilos entran en la sección crítica simultáneamente.
Correcto:
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// Ahora todos los hilos usan el mismo objeto lock
// y realmente se esperan unos a otros.
}
}
Error n.º 2: sincronizar sobre un literal de cadena
public void doSomething() {
synchronized ("lock") {
// Los literales de cadena están internados: distintas partes del programa pueden
// sincronizarse accidentalmente sobre la misma cadena.
}
}
Conclusión:
Sincroniza solo sobre objetos privados, creados específicamente para ello, que no se usen en ningún otro lugar.
3. Interbloqueo (deadlock): «Tú primero — yo a ti, y ambos parados»
Deadlock (interbloqueo) es un clásico. Dos (o más) hilos adquieren bloqueos distintos de manera alterna y se esperan mutuamente, hasta que el programa queda paralizado.
Ejemplo:
public class DeadlockExample {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized (lockA) {
// Esperamos un poco para que el experimento sea más claro
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (lockB) {
// ...
}
}
}
public void method2() {
synchronized (lockB) {
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (lockA) {
// ...
}
}
}
}
Si un hilo llama a method1() y otro a method2(), el primero tomará lockA y esperará a lockB, y el segundo hará lo contrario. Como resultado, ambos se esperarán mutuamente para siempre.
¿Cómo evitarlo?
- Adquiere los bloqueos siempre en el mismo orden en todos los hilos.
- Minimiza el número de bloqueos retenidos simultáneamente.
- Usa herramientas de diagnóstico (por ejemplo, jstack) si el programa se ha quedado colgado.
Analogía:
Es como si dos personas se encontraran en un pasillo estrecho y cada una decidiera ceder el paso, pero solo si la otra cede primero. Al final ambos se quedan parados esperando a que alguien ceda.
4. Sincronización excesiva: «¿Más vale pasarse que quedarse corto?» — ¡no siempre!
A veces, por miedo a errores, los desarrolladores sincronizan todo. Como resultado, el rendimiento cae y el beneficio es nulo.
Ejemplo:
public synchronized void add(int value) {
// Aquí solo hay una línea que no requiere sincronización.
System.out.println("Añadido: " + value);
}
En este caso, la sincronización no es necesaria: la salida a pantalla mediante System.out.println ya es segura para hilos, y el propio método no trabaja con recursos compartidos.
¿Dónde es crítico?
Si sincronizas métodos que se invocan con frecuencia y no requieren protección, reduces drásticamente el rendimiento del programa. Los hilos se ponen en cola cuando podrían trabajar en paralelo.
Mejor práctica:
Sincroniza solo lo que realmente sea necesario. La sección crítica debe ser lo más pequeña posible.
5. Uso incorrecto de volatile: «Hay visibilidad, pero no hay atomicidad»
El modificador volatile en Java garantiza que los cambios de la variable sean visibles para todos los hilos. Pero no garantiza la atomicidad de las operaciones.
Error:
private volatile int counter = 0;
public void increment() {
counter++; // ¡No es atómico!
}
La operación counter++ consiste en leer el valor, incrementarlo y escribirlo de nuevo. Si dos hilos ejecutan este código simultáneamente, el valor final puede ser menor que el esperado.
Correcto:
Para operaciones atómicas, usa synchronized, AtomicInteger u otras clases seguras para hilos.
import java.util.concurrent.atomic.AtomicInteger;
private final AtomicInteger counter = new AtomicInteger();
public void increment() {
counter.incrementAndGet();
}
¿Cuándo usar volatile?
Para flags simples (por ejemplo, «terminar la ejecución»), cuando no se requiere atomicidad.
GO TO FULL VERSION