La sincronización reúne los mecanismos que coordinan el acceso de varios hilos al estado compartido.
En el artículo anterior aprendimos a crear hilos para hacer varias tareas de forma concurrente.
Si esos hilos son independientes, no comparten este problema. Cuando dos hilos acceden al mismo estado y al menos uno escribe, necesitamos coordinar ese acceso.
Bienvenido al mundo de las Race Conditions (Condiciones de Carrera).
El problema: race conditions
Imagina un contador simple. Queremos que dos hilos sumen 1.000 visitas cada uno a una web. El resultado final debería ser 2.000.
class Contador {
private int cuenta = 0;
public void incrementar() {
cuenta++; // El peligro está aquí
}
public int getCuenta() { return cuenta; }
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Contador c = new Contador();
// Hilo 1: Suma 1000 veces
Thread t1 = new Thread(() -> {
for(int i=0; i<1000; i++) c.incrementar();
});
// Hilo 2: Suma 1000 veces
Thread t2 = new Thread(() -> {
for(int i=0; i<1000; i++) c.incrementar();
});
t1.start();
t2.start();
t1.join(); // Esperamos a que terminen
t2.join();
System.out.println("Total visitas: " + c.getCuenta());
}
}
Si ejecutas esto, esperarías ver 2000.
Puedes ver 2000 o un valor menor, como 1950, 1890 o 1999. El resultado no está garantizado y puede cambiar entre ejecuciones.
¿Por qué falla? (atomicidad)
El problema es que la operación cuenta++ NO es atómica. Para la CPU, sumar 1 implica tres pasos distintos:
- Leer el valor actual de la memoria (ej: 10).
- Sumar 1 al valor (10 + 1 = 11).
- Escribir el nuevo valor en memoria (11).
Si dos hilos entran a la vez, ocurre el desastre:
- Hilo A lee 10.
- Hilo B lee 10 (antes de que A escriba).
- Hilo A suma y escribe 11.
- Hilo B suma (sobre su 10) y escribe 11.
Resultado: Han hecho dos sumas, pero el contador solo ha subido 1. Hemos “perdido” una visita.
La solución: synchronized
Para arreglarlo, convertimos incrementar() en una sección crítica. Solo un hilo podrá modificar la cuenta con el mismo cerrojo cada vez, y los cambios tendrán la visibilidad definida por el modelo de memoria.
En Java, esto se logra con la palabra clave synchronized.
class ContadorSeguro {
private int cuenta = 0;
// Solo un hilo puede ejecutar este método sobre la misma instancia cada vez
public synchronized void incrementar() {
cuenta++;
}
public int getCuenta() { return cuenta; }
}
Al añadir synchronized, Java crea un Cerrojo (Monitor Lock) asociado al objeto.
Bloques synchronized (sincronización fina)
Sincronizar un método entero es fácil, pero a veces es ineficiente. Si tu método tarda 5 segundos en ejecutarse, paralizas a todos los demás hilos durante 5 segundos.
A veces solo quieres proteger una línea concreta. Puedes usar un Bloque Synchronized.
public void metodoComplejo() {
System.out.println("Preparando datos..."); // Esto se puede hacer en paralelo
// Solo bloqueamos el acceso a la variable compartida
// 'this' es el objeto que usamos como llave
synchronized(this) {
cuenta++;
}
System.out.println("Guardando logs..."); // Esto también en paralelo
}
Esto mejora el rendimiento porque el “bloqueo” dura lo mínimo posible.
El coste de la sincronización
Quizás pienses: “Pues pongo synchronized a todos mis métodos y me curo en salud”.
Mala idea.
- Rendimiento: Adquirir y soltar cerrojos consume CPU. Además, conviertes tu programa multihilo en uno secuencial (si todos hacen cola, nadie trabaja en paralelo).
- Deadlocks (Abrazo Mortal): Si el Hilo A tiene la llave del recurso 1 y espera al 2, y el Hilo B tiene la llave del 2 y espera al 1… ambos se quedan congelados para siempre. El programa se cuelga.
Alternativas modernas: atomicidad sin bloqueo
Para casos simples como contadores, Java incluye clases en java.util.concurrent.atomic que ofrecen operaciones atómicas sin usar un monitor. Su rendimiento depende de la contención; no son universalmente más rápidas que synchronized.
import java.util.concurrent.atomic.AtomicInteger;
class ContadorAtomico {
// Variable especial thread-safe
private AtomicInteger cuenta = new AtomicInteger(0);
public void incrementar() {
// Equivale a cuenta++ pero es atómico y seguro
cuenta.incrementAndGet();
}
}