1. Iniciación de hilos dentro de constructores
Un error frecuente en el diseño de clases es arrancar un hilo directamente en el constructor. Esta práctica puede comprometer la integridad del objeto, especialmente en jerarquías de herencia.
public class ProcesadorBase {
protected int estadoInicial;
public ProcesadorBase() {
this.estadoInicial = 10;
Thread tarea = new Thread(new TareaInterna());
tarea.start();
}
private class TareaInterna implements Runnable {
public void run() {
// Riesgo: Se accede a 'estadoInicial' antes de que
// una subclase termine su propia inicialización.
System.out.println("Estado: " + estadoInicial);
}
}
}
Si una clase extiende de ProcesadorBase, el hilo podría comenzar a ejecutarse antes de que el constructor de la subclase haya finalizado. Esto provoca que el hilo trabaje con un objeto parcialmente construido. Para evitarlo, es recomendable utilizar un método de fábrica estático o un método start() independiente, o bien declarar la clase como final.
2. Sincronización incompleta de accesos
Muchos desarrolladores asumen que sincronizar solo el método de escritura (setter) es suficiente. Sin embargo, sin sincronizar el lector (getter), no hay garantía de visibilidad entre hilos.
class AlmacenDatos {
private long contador;
public long obtenerContador() {
return contador; // Falta sincronización: puede leer un valor obsoleto
}
public synchronized void incrementar(long valor) {
this.contador += valor;
}
}
En el caso de tipos de 64 bits como long o double, el riesgo es mayor, ya que la JVM puede realizar la lectura o escritura en dos pasos de 32 bits, lo que podría resultar en la lectura de un valor corrupto (lectura parcial). La solución es sincronizar ambos métodos o declarar la variable como volatile si la operación es una simple asignación.
3. Mutación del objeto de bloqueo
Un error sutil pero devastador es cambiar la referencia del objeto que se está utilizando como monitor dentro de un bloque sincronizado.
private Object cerrojo = new Object();
public void ejecutarTarea() {
synchronized(cerrojo) {
// ... lógica de negocio ...
cerrojo = new Object(); // Error: el próximo hilo usará un monitor distinto
}
}
Cuando se reasigna la variable cerrojo, otros hilos que intenten entrar al bloque se sincronizarán sobre una instancia diferente, rompiendo la exclusión mutua. Para prevenir esto, los objetos utilizados como candados deben declararse siempre como final.
4. Omisión del bucle en llamadas a wait()
El uso de if para evaluar una condición antes de llamar a wait() es una vulnerabilidad conocida debido a los "despertares espuiros" (spurious wakeups) o notificaciones enviadas antes de que el hilo esté listo.
synchronized(monitor) {
// Uso incorrecto de 'if'
if (colaDeTareas.isEmpty()) {
monitor.wait();
}
procesar(colaDeTareas.poll());
}
Si el hilo despierta inesperadamente y la cola sigue vacía, poll() podría fallar. El estándar de oro es utilizar un bucle while para reevaluar la condición inmediatamente después de despertar:
synchronized(monitor) {
while (colaDeTareas.isEmpty()) {
monitor.wait();
}
procesar(colaDeTareas.poll());
}
5. Granularidad de sincronización errónea
Un error común es creer que combinar varias operaciones atómicas resulta en una operación compuesta atómica. Este concepto se resume en: Atomic + Atomic != Atomic.
Map<String, String> mapaSeguro = Collections.synchronizedMap(new HashMap<>());
// Error de condición de carrera entre el check y el put
if (!mapaSeguro.containsKey("config")) {
mapaSeguro.put("config", "valor_por_defecto");
}
Aunque containsKey y put son seguros individualmente, otro hilo podría insertar la llave entre ambas llamadas. Para corregirlo, se debe sincronizar sobre el objeto del mapa o utilizar métodos modernos como putIfAbsent de ConcurrentHashMap.
6. Uso indebido de la palabra clave volatile
volatile garantiza la visibilidad y el orden de las instrucciones, pero no la atomicidad. Es ideal para banderas de estado, pero peligroso para contadores.
// Uso INCORRECTO de volatile para contadores
private volatile int peticionesTotales;
public void registrarPeticion() {
peticionesTotales++; // Esta operación no es atómica (lectura-modificación-escritura)
}
Para operaciones incrementales, es imperativo usar synchronized o clases del paquete java.util.concurrent.atomic como AtomicInteger.
Del mismo modo, volatile no puede proteger invariantes que involucren múltiples variables. Si el estado de una variable depende del valor actual de otra, como en un rango numérico (min < max), se requeire sincronización fuerte para evitar estados inconsistentes durante actualizaciones concurrentes.