1. La palabra clave synchronized: para qué y cómo
En Java, la palabra clave synchronized es como un cartel de «¡Ocupado!» en la puerta del baño: mientras un hilo se encuentra dentro de la «sección crítica», los demás esperan educadamente su turno. Solo cuando el primero salga, el siguiente podrá entrar y ejecutar su código.
Sintaxis: bloque y método
Bloque sincronizado
synchronized (object) {
// sección crítica
}
- object es cualquier objeto sobre el que quieras «colgar un candado». Mientras un hilo ejecute este bloque, otros hilos que quieran entrar a un bloque con ese mismo objeto esperarán.
Método sincronizado
public synchronized void increment() {
// sección crítica
}
- Aquí el «candado» se cuelga en el propio objeto (this). Es decir, solo un hilo a la vez puede ejecutar cualquier método sincronizado de ese objeto.
Método estático sincronizado
public static synchronized void foo() {
// sección crítica
}
- Aquí el bloqueo ocurre a nivel de clase (ClassName.class), no de un objeto concreto.
Cómo funciona por dentro
Cuando un hilo entra en un bloque o método sincronizado, captura el «monitor» del objeto (o de la clase para métodos estáticos). Si el monitor ya está ocupado, el hilo espera. En cuanto el monitor se libera, el siguiente hilo puede entrar.
2. Ejemplo: incremento de un contador con y sin sincronización
Sin sincronización
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class CounterDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Valor final: " + counter.getCount());
}
}
Valor esperado: 2000
Valor real: puede ser menor (por ejemplo, 1995, 1987...), y en cada ejecución — su propia «sorpresa».
¿Por qué? Porque la operación count++ no es atómica: se divide en tres acciones — leer el valor, incrementarlo, escribirlo de nuevo. Si dos hilos lo hacen a la vez, pueden «pisar» el resultado del otro.
Solución: synchronized
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Ahora solo un hilo a la vez puede ejecutar el método increment(). El valor final siempre será 2000.
Alternativa: bloque sincronizado
public class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
}
El resultado será el mismo. Puedes sincronizar no todo el método, sino solo la parte necesaria.
3. Introducción al «monitor del objeto»
El monitor es un «candado» incorporado en cada objeto en Java. Cuando escribes synchronized(object), el hilo intenta «cerrar con llave» ese objeto. Si el candado está libre, el hilo lo obtiene; si no, espera su turno. En cuanto el hilo sale del bloque, el candado se libera.
¡Importante! Si sincronizas sobre objetos diferentes, los hilos no se esperarán entre sí. Por eso es fundamental elegir el objeto correcto para sincronizar.
Métodos estáticos sincronizados
A veces el recurso compartido no es un objeto, sino algo común a todas las instancias de la clase (por ejemplo, una variable estática). En ese caso, la sincronización debe ser a nivel de clase.
public class StaticCounter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
Esto es equivalente a:
public static void increment() {
synchronized (StaticCounter.class) {
count++;
}
}
El monitor se cuelga sobre el objeto de la clase (Class), no sobre una instancia concreta.
4. La palabra clave volatile: qué es y para qué sirve
El problema de la visibilidad entre hilos
En Java, cada hilo puede almacenar en caché valores de variables para acelerar la ejecución. Esto significa que, si un hilo cambia una variable, otro hilo puede «no darse cuenta», porque sigue leyendo el valor de su caché local. Esto es especialmente crítico para banderas con las que los hilos se señalan entre sí.
Cómo funciona volatile
Si una variable se declara como volatile, significa que:
- Todos los hilos siempre la leen y escriben en la memoria principal, sin pasar por la caché.
- Cualquier cambio de la variable se hace visible inmediatamente para todos los hilos.
¡Pero! Las operaciones con volatile por sí solas no son atómicas (excepto la lectura/escritura simple de primitivos como boolean, int, etc.). Si haces algo más complejo que una asignación, necesitas sincronización.
Ejemplo: indicador de finalización
public class Worker extends Thread {
private volatile boolean running = true;
public void run() {
while (running) {
// hacemos algo útil
}
System.out.println("Hilo finalizado");
}
public void shutdown() {
running = false;
}
}
Worker w = new Worker();
w.start();
// ... al cabo de un tiempo
w.shutdown();
Sin volatile, el hilo puede «no notar» el cambio de la bandera y quedarse en un bucle infinito (especialmente en sistemas multinúcleo). Con volatile, todo funciona como debe.
5. Limitaciones de volatile: no atomicidad
Muchos principiantes piensan: «Si hago volatile un int, entonces puedo escribir count++ y no preocuparme». Por desgracia, no es así:
private volatile int count = 0;
public void increment() {
count++;
}
¡Error! La operación count++ sigue sin ser atómica: son tres pasos: (1) leer, (2) incrementar, (3) escribir de nuevo. Si dos hilos leen el mismo valor a la vez, ambos lo incrementarán y ambos escribirán el mismo resultado: se «perderá» un incremento.
Conclusión: volatile solo garantiza la visibilidad de los cambios, pero no protege de condiciones de carrera en operaciones complejas.
6. Cuándo usar synchronized y cuándo — volatile
- volatile: cuando tienes una bandera simple (por ejemplo, boolean) que un hilo escribe y otro lee. Ejemplos: terminar un hilo, señalizar un evento.
- synchronized: cuando necesitas garantizar la atomicidad de operaciones complejas (por ejemplo, un incremento, cambios de varias variables, trabajo con estructuras de datos).
Tabla para recordar
| Escenario | volatile | synchronized |
|---|---|---|
| Pasar una señal entre hilos | ✔ | ✔ |
| Operación atómica (incremento) | ✖ | ✔ |
| Múltiples pasos en la sección crítica | ✖ | ✔ |
| Solo visibilidad de cambios | ✔ | ✔ |
7. Errores típicos al usar synchronized y volatile
Error n.º 1: Sincronización sobre el objeto incorrecto. Si sincronizas sobre una variable local o sobre un objeto distinto en cada hilo, no tendrás ninguna protección.
Object lock = new Object();
synchronized (lock) {
// ...
}
Si cada hilo crea su propio lock, no sirve de nada. Necesitas un único punto de sincronización: un objeto común para todos los hilos.
Error n.º 2: Esperar atomicidad de volatile. volatile garantiza visibilidad, no atomicidad. Operaciones como count++ siguen siendo inseguras sin sincronización.
Error n.º 3: Sincronizar una región de código demasiado grande. Si sincronizas todo el método cuando solo hace falta una línea, bloqueas innecesariamente a otros hilos y pierdes rendimiento. Intenta reducir la «sección crítica».
Error n.º 4: Olvidar hacer la sincronización «estática» para datos estáticos. Si tienes una variable estática y sincronizas sobre this, no ayudará. Para datos estáticos, la sincronización debe ser a nivel de clase: synchronized(ClassName.class).
Error n.º 5: Sincronizar sobre un literal de cadena. Sincronizar sobre cadenas es peligroso, porque literales idénticos se internan en la JVM. Puedes obtener por accidente un bloqueo común para distintas partes del programa.
GO TO FULL VERSION