Synchronized es un mecanismo de sincronización integrado en el lenguaje Java que proporciona acceso exclusivo a secciones críticas de código. Según Oracle, 2024, el modificador synchronized garantiza que solo un hilo puede ejecutar el método o bloque marcado en un momento específico. Este mecanismo se basa en monitores, un concepto fundamental de los sistemas operativos que asegura el correcto funcionamiento de aplicaciones multihilo de todos los niveles de complejidad.
Puntos clave
Synchronized es una palabra clave en Java que garantiza que solo un hilo ejecute una sección protegida de código en un momento dado, evitando la corrupción de datos durante el acceso concurrente. Apareció en la primera versión de Java y sigue siendo la forma más sencilla de garantizar la seguridad entre hilos para desarrolladores de cualquier nivel.
El modificador synchronized resuelve dos tareas: exclusión mutua y visibilidad de cambios. Cuando un hilo sale de un bloque synchronized, todos los cambios son visibles para otros hilos que entren en un bloque sincronizado en el mismo objeto.
Synchronized se puede aplicar a un método completo o a un bloque de código arbitrario especificando un objeto monitor. En ambos casos, la JVM inserta instrucciones monitorenter y monitorexit a nivel de bytecode.
En aplicaciones multihilo sin sincronización, se produce una condición de carrera (race condition) cuando dos hilos modifican simultáneamente los mismos datos, dando lugar a resultados impredecibles. Synchronized se convirtió en la primera y principal herramienta de Java para combatir este problema, proporcionando una sintaxis declarativa simple accesible a cualquier desarrollador.
El mecanismo synchronized se basa en el concepto de monitor, un primitivo de sincronización de alto nivel incorporado en cada objeto Java. El monitor se asocia a un objeto cuando se usa por primera vez un bloque synchronized sobre él.
Cada objeto en Java tiene un monitor asociado. Cuando un hilo entra en un bloque synchronized, adquiere el monitor del objeto. Si el monitor ya está ocupado por otro hilo, el hilo se bloquea hasta que se libera. En bytecode, esto corresponde al par de instrucciones monitorenter y monitorexit.
La JVM optimiza synchronized a través de varios niveles: biased locking (bloqueo sesgado) para acceso de un solo hilo, lightweight locking (bloqueo ligero) para baja contención, y heavyweight locking (bloqueo pesado) para contención intensa con participación del SO. Estos niveles mejoran el rendimiento sin cambiar el código.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized establece una relación happens-before: todas las acciones en un hilo antes de salir de un bloque synchronized son visibles para otro hilo después de entrar en un bloque sincronizado en el mismo objeto. Esto garantiza no solo exclusión mutua sino también consistencia de datos para todos los hilos.
Java ofrece dos formas de aplicar synchronized: a nivel de método y a nivel de bloque. La elección entre ellos afecta al rendimiento y la granularidad de la sincronización.
Marcar un método con el modificador synchronized lo sincroniza automáticamente en la instancia actual (para métodos de instancia) o en el objeto Class (para métodos estáticos). Es la forma más simple de garantizar exclusión mutua, pero a menudo es excesiva si la sección crítica constituye solo una pequeña parte del método y el resto del código no requiere sincronización.
Un bloque synchronized ofrece control preciso: especificas el objeto monitor y sincronizas solo la sección necesaria, dejando el resto del método fuera del bloqueo. Esto minimiza el tiempo de retención del monitor y mejora el rendimiento general de la aplicación en un entorno multihilo, ya que otros hilos pueden ejecutar código no relacionado en paralelo sin esperar la liberación del monitor.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// código fuera de la sección crítica - sin sincronización
prepareData()
synchronized (lock) {
// solo este bloque está protegido
updateSharedState()
}
// continuación sin bloqueo
cleanup()
}
}
| Criterio | Método synchronized | Bloque synchronized |
|---|---|---|
| Monitor | this (instancia) o Class | cualquier objeto |
| Granularidad | método completo | solo el código necesario |
| Legibilidad | alta | media |
| Rendimiento | menor para métodos grandes | mayor para secciones críticas pequeñas |
En el desarrollo de Android, synchronized se usa ampliamente para proteger SharedPreferences, el acceso a bases de datos y componentes de la interfaz de usuario. Sin embargo, su uso en el hilo principal está totalmente desaconsejado debido al riesgo de congelación de la interfaz.
SharedPreferences en Android proporciona seguridad básica entre hilos, pero al editar desde varios hilos a través de Editor, puede ser necesaria una sincronización externa. Un bloque synchronized con un objeto de bloqueo separado garantiza la consistencia de los cambios.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
La principal limitación de synchronized en Android es el bloqueo de hilo. A diferencia de las corrutinas con Mutex, synchronized bloquea el hilo del sistema por completo. En el hilo principal, esto causa ANR. En el desarrollo moderno de Android, se recomienda reemplazar synchronized por corrutinas (suspend Mutex) o tipos atómicos (AtomicInteger).
Java y Kotlin modernos ofrecen varias alternativas a synchronized, cada una resolviendo los mismos problemas con menos limitaciones o mejor rendimiento.
La interfaz Lock con las implementaciones ReentrantLock y ReadWriteLock proporciona tiempos de espera, espera interrumpible y múltiples colas Condition. Es más flexible que synchronized pero requiere liberación explícita en finally, lo que aumenta el riesgo de error si se olvida el unlock.
AtomicInteger, AtomicLong, AtomicReference y otras clases utilizan algoritmos Lock-Free basados en CAS (Compare-And-Swap). Son significativamente más rápidos que synchronized en escenarios de contención moderada porque no bloquean hilos sino que realizan reintentos optimistas sin requerir cambios de contexto del núcleo del SO.
ThreadLocal proporciona un enfoque alternativo: cada variable ThreadLocal está aislada dentro de un único hilo y no requiere sincronización para lectura y escritura. Esto elimina por completo la necesidad de synchronized para datos que no deben compartirse entre hilos. ThreadLocal se usa activamente en frameworks (Spring, Hibernate) para almacenar contexto de transacciones y sesiones.
En proyectos Kotlin para Android, una alternativa a synchronized es Mutex de kotlinx.coroutines. No bloquea el hilo del sistema operativo sino que suspende la corrutina hasta que se libera el bloqueo, lo que permite un uso eficiente de los hilos del pool y evita ANR durante esperas prolongadas de liberación de recursos.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
El rendimiento de synchronized ha cambiado significativamente en las versiones recientes de Java. Antes se consideraba un mecanismo “pesado”, pero las JVM modernas han eliminado la mayor parte de la sobrecarga mediante optimizaciones avanzadas del compilador JIT. Veamos en detalle cómo la máquina virtual acelera el código sincronizado en tiempo de ejecución.
El compilador JIT de la JVM aplica varias optimizaciones: biased locking elimina la sincronización si el bloqueo siempre lo adquiere el mismo hilo; lock coarsening fusiona bloques synchronized adyacentes en uno solo; lock elimination elimina la sincronización si el objeto solo es accesible por un hilo. Estas optimizaciones hacen que synchronized sea prácticamente gratuito con baja contención.
La JVM determina el nivel de contención para cada objeto: cuando no hay contención, se activa biased locking; cuando aparece un segundo hilo, el bloqueo pasa a modo ligero con espera activa (spin); y solo durante espera prolongada se escala a pesado con un mutex del sistema. Esta escalada ocurre automáticamente, y el desarrollador no necesita elegir manualmente una estrategia.
En benchmarks modernos (Java 17+), synchronized muestra un rendimiento comparable a ReentrantLock con contención baja y moderada. Con alta contención, Lock puede tener ventaja gracias a una cola de espera más eficiente con soporte de timeouts e interrupción. Para sistemas de alta carga donde la contención es constante, ReentrantLock con modo fair ofrece un comportamiento más predecible.
Las clases atómicas (AtomicInteger, AtomicReference) siguen siendo las más rápidas para contadores y banderas simples gracias a la implementación Lock-Free basada en CAS. No bloquean hilos en absoluto: en caso de conflicto, la operación simplemente se reintenta en un bucle. Esto proporciona un aumento de rendimiento de 3 a 5 veces en comparación con synchronized en operaciones de incremento de contador con 4-8 hilos.
Preguntas frecuentes
Synchronized proporciona tanto exclusión mutua como visibilidad. Volatile solo garantiza visibilidad de cambios: escribir en una variable volatile es visible para todos los hilos pero no evita la modificación simultánea, es decir, no protege contra condiciones de carrera.
Sí, es posible un deadlock con sincronización anidada usando diferentes órdenes de monitores. Por ejemplo, un hilo llama a synchronized(a) { synchronized(b) }, mientras que otro llama a synchronized(b) { synchronized(a) }. Evita bloques synchronized anidados o fija un orden de monitores consistente.
Un monitor es un mecanismo de sincronización asociado a cada objeto Java. Garantiza que solo un hilo ejecuta código synchronized en ese objeto. El monitor incluye un bloqueo, una cola de espera y un conjunto de hilos esperando notificación mediante wait/notify.
En versiones modernas de Java (17+), synchronized no va por detrás de Lock en rendimiento gracias a las optimizaciones JIT (biased locking, lock coarsening). Lock se prefiere no por velocidad sino por capacidades adicionales: timeouts, espera interrumpible y múltiples colas Condition.
Un método estático synchronized usa el monitor del objeto Class de la clase dada, no de la instancia. Esto significa que la sincronización se aplica a todas las instancias de la clase. Los métodos synchronized no estáticos y estáticos usan diferentes monitores y no se bloquean entre sí.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también