«¿Qué significa synchronized?» — la pregunta que atormenta a uno de cada dos juniors
\n\nSi alguna vez has escrito una aplicación multihilo en Java, seguramente te has encontrado con esta situación: dos hilos modifican una misma variable al mismo tiempo, y el resultado son errores que se reproducen una de cada cien ejecuciones. ¿Te suena? Precisamente para eso existe la palabra clave synchronized. Pero, ¿qué hace realmente? ¿Cómo funciona «bajo el capó»? Y lo más importante: ¿cuándo usarla y cuándo es mejor prescindir de ella?
En este artículo no solo repetiremos la documentación. Analizaremos una pregunta popular de Stack Overflow (esa que ha acumulado miles de visitas), la adaptaremos a un formato educativo y mostraremos una comparación de enfoques: sincronización a nivel de método, a nivel de bloque y alternativas de java.util.concurrent. ¡Allá vamos!
El problema: condición de carrera (Race Condition)
\n\nImagina: tienes un contador que incrementan dos hilos. El código es simple, pero el resultado es inesperado.
\n\npublic class Counter {\n private int count = 0;\n\n public void increment() {\n count++;\n }\n\n public int getCount() {\n return count;\n }\n}\n\nEjecutamos dos hilos, cada uno hace 1000 incrementos. En teoría, debería ser 2000. Pero en la práctica obtenemos 1987, 1993 o incluso 1765. ¿Por qué? Porque la operación count++ no es atómica. Consta de tres pasos: lectura → incremento → escritura. Los hilos se «cruzan» y una modificación se pierde.
Esto es una race condition — condición de carrera. Y synchronized es una de las principales herramientas para combatirla.
¿Qué hace synchronized? Miremos bajo el capó
\n\nLa palabra clave synchronized en Java garantiza exclusión mutua (mutex) y visibilidad de cambios entre hilos. Cuando un hilo entra en un bloque synchronized, adquiere el monitor del objeto (o de la clase). Otros hilos que intenten entrar en cualquier bloque synchronized sobre el mismo monitor se bloquean hasta que se libere.
Importante: synchronized solo funciona con un monitor. Si tienes dos objetos diferentes, la sincronización no funcionará.
Dos niveles de sincronización
\n- \n
- Método sincronizado — bloquea todo el método, usa el monitor del objeto (
this). \n - Bloque sincronizado — bloquea solo una parte del código, puedes especificar cualquier objeto como monitor. \n
Comparemos ambos en la práctica.
\n\nComparación de enfoques: método vs bloque
\n\n1. Método sincronizado
\n\nLa forma más simple es agregar synchronized a la firma del método.
public class SafeCounter {\n private int count = 0;\n\n public synchronized void increment() {\n count++;\n }\n\n public synchronized int getCount() {\n return count;\n }\n}\n\nVentajas:
\n- \n
- Mínimo código, fácil de leer. \n
- Garantiza que todo el método sea una operación atómica. \n
Desventajas:
\n- \n
- Bloqueo «pesado» — incluso si dentro del método solo hay una línea, el hilo se bloquea durante toda la ejecución. \n
- Reduce el rendimiento si el método es largo o se llama con frecuencia. \n
- No es flexible — no se puede sincronizar solo una parte de la lógica. \n
2. Bloque sincronizado
\n\nUn control más preciso. Tú decides qué objeto usar como monitor.
\n\npublic class SafeCounterWithBlock {\n private int count = 0;\n private final Object lock = new Object();\n\n public void increment() {\n synchronized (lock) {\n count++;\n }\n }\n\n public int getCount() {\n synchronized (lock) {\n return count;\n }\n }\n}