Lock es un mecanismo de sincronización que proporciona acceso exclusivo a secciones críticas de código en aplicaciones multiproceso. Según Oracle, 2024, la interfaz Lock ofrece un control de sincronización más flexible en comparación con los bloques synchronized tradicionales, incluyendo intentos de adquisición con tiempo de espera y soporte para múltiples colas de espera.
Puntos Clave
Lock es una interfaz del paquete java.util.concurrent.locks que proporciona operaciones explícitas de bloqueo y desbloqueo para sincronizar el acceso a datos. A diferencia de synchronized, Lock le da al desarrollador control total sobre el mecanismo de bloqueo.
La interfaz Lock se introdujo en Java 5 como alternativa al mecanismo synchronized integrado. Los métodos principales son lock, unlock, tryLock y lockInterruptibly. Los bloqueos permiten organizar el acceso seguro a datos en un entorno multiproceso, evitando condiciones de carrera y corrupción de datos.
La principal ventaja de Lock sobre synchronized es la flexibilidad. El desarrollador puede intentar adquirir un bloqueo con tiempo de espera, verificar su disponibilidad sin bloquearse u organizar múltiples colas de espera con diferentes prioridades.
Antes de que la interfaz Lock apareciera en Java 5, el único método de sincronización era synchronized, que sufría limitaciones: sin tiempos de espera, sin espera interrumpible y una sola cola. Doug Lea diseñó el paquete java.util.concurrent, incluyendo Lock como bloque fundamental.
Un bloqueo gestiona el acceso a través de una bandera de estado interna y una cola de espera. Cuando un hilo llama a lock(), el mecanismo verifica si el bloqueo está libre y lo adquiere o coloca el hilo en la cola hasta que se libere.
En el núcleo de cualquier bloqueo se encuentra una operación atómica de comparación e intercambio (CAS). Al llamar a lock(), el hilo intenta establecer atómicamente la bandera de ocupado. Si la bandera ya está establecida, el hilo se bloquea. Al llamar a unlock(), la bandera se limpia y se despierta un hilo en espera.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// sección crítica
println("El hilo ${Thread.currentThread().name} está trabajando")
} finally {
lock.unlock()
}
}
ReentrantLock utiliza internamente una lista doblemente enlazada (cola de bloqueo CLH) donde cada hilo en espera está representado por un nodo. Cuando el bloqueo se libera, se despierta el nodo principal de la cola. El modo justo (fair) garantiza el orden FIFO, mientras que el modo injusto permite que un nuevo hilo adquiera el bloqueo antes que los que esperan para mejorar el rendimiento.
En el ecosistema Java moderno, existen varias implementaciones de bloqueos, cada una optimizada para escenarios específicos. Elegir el bloqueo correcto impacta directamente en el rendimiento y la fiabilidad de una aplicación multiproceso.
ReentrantLock es la implementación de Lock básica y más utilizada. Admite la reentrada por el mismo hilo: si un hilo ya posee el bloqueo, llamar a lock() nuevamente no lo bloquea. Esto evita deadlocks en llamadas recursivas.
ReadWriteLock separa los bloqueos en dos modos: lectura y escritura. Varios hilos pueden mantener el bloqueo de lectura simultáneamente, pero la escritura requiere acceso exclusivo. Esto mejora significativamente el rendimiento bajo lecturas frecuentes y escrituras escasas.
StampedLock es la implementación más nueva, introducida en Java 8. Admite tres modos: escritura, lectura y lectura optimista. La lectura optimista no bloquea otros hilos y valida los datos después de leer, proporcionando una mejora de rendimiento del 10-20% sobre ReadWriteLock.
| Bloqueo | Versión Java | Modos | Rendimiento |
|---|---|---|---|
| ReentrantLock | Java 5 | exclusivo | alto |
| ReadWriteLock | Java 5 | lectura + escritura | medio |
| StampedLock | Java 8 | lectura + escritura + optimista | muy alto |
ReentrantLock es la implementación de Lock más popular, que ofrece varias características no disponibles en synchronized. Comprender sus características es esencial para trabajar eficazmente con subprocesos múltiples.
El constructor de ReentrantLock acepta un parámetro fair. Cuando es true, el bloqueo garantiza orden FIFO; cuando es false, un nuevo hilo puede adquirir el bloqueo antes que los que esperan. El modo justo evita la inanición pero reduce el rendimiento entre un 10 y un 20% debido a la sobrecarga de mantener la cola.
A diferencia de synchronized, ReentrantLock admite tryLock con tiempo de espera. Si el bloqueo no se puede adquirir dentro del tiempo especificado, el hilo continúa su ejecución en lugar de bloquearse indefinidamente. El método lockInterruptibly permite interrumpir un hilo en espera mediante Thread.interrupt().
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Bloqueo adquirido")
} finally {
lock.unlock()
}
} else {
println("No se pudo adquirir el bloqueo")
}
}
ReentrantLock admite múltiples variables de condición mediante el método newCondition(). Cada Condition tiene su propia cola de espera, lo que permite escenarios de activación complejos. Los métodos await() y signal() reemplazaron a wait() y notify() de los bloques synchronized, pero con soporte para múltiples colas.
ReadWriteLock y StampedLock abordan la optimización del acceso cuando las lecturas predominan sobre las escrituras. Son significativamente más eficientes que ReentrantLock en escenarios donde la lectura ocurre con más frecuencia que la escritura.
La interfaz ReadWriteLock contiene dos métodos: readLock() y writeLock(). El bloqueo de lectura puede ser mantenido por varios hilos simultáneamente, mientras que el bloqueo de escritura es exclusivo. Un ejemplo típico es un caché seguro para subprocesos: muchos hilos leen datos mientras solo uno los actualiza periódicamente.
class SafeCache<K, V> {
private val map = mutableMapOf<K, V>()
private val rwLock = ReentrantReadWriteLock()
fun get(key: K): V? {
rwLock.readLock().lock()
return try { map[key] } finally { rwLock.readLock().unlock() }
}
fun put(key: K, value: V) {
rwLock.writeLock().lock()
return try { map[key] = value } finally { rwLock.writeLock().unlock() }
}
}
StampedLock añade un tercer modo — tryOptimisticRead. Este modo no bloquea otros hilos, sino que simplemente registra un sello (stamp) de estado. Después de leer, el desarrollador llama a validate(stamp) para verificar si los datos cambiaron durante la lectura. Si los datos cambiaron, la operación debe repetirse.
En aplicaciones móviles, los bloqueos se utilizan para coordinar el acceso a datos compartidos entre subprocesos. Sin embargo, su uso requiere especial precaución debido a los recursos limitados del dispositivo y la necesidad de mantener la capacidad de respuesta de la interfaz.
En Android, ReentrantLock es útil al trabajar con Room, cachés y archivos. Es importante recordar: nunca adquiera un bloqueo en el hilo principal. Para código asíncrono, son preferibles las corrutinas y Mutex de kotlinx.coroutines, que suspenden la corrutina en lugar de bloquear el hilo.
En iOS, el NSLock estándar se usa con menos frecuencia: los desarrolladores prefieren DispatchQueue con banderas barrier u os_unfair_lock. Swift 5.7+ proporciona mecanismos de sincronización modernos a través de actores, que protegen el estado automáticamente.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
Para evitar deadlocks, siga un orden de adquisición consistente en todo el proyecto. Use tryLock con tiempo de espera en lugar de lock() siempre que sea posible un bloqueo prolongado. Considere usar algoritmos Lock-Free (AtomicReference, ConcurrentHashMap) en lugar de bloqueos tradicionales.
El uso de Lock requiere disciplina y el cumplimiento de varias reglas que previenen deadlocks y degradación del rendimiento. Estas prácticas han sido desarrolladas por la comunidad Java durante 20 años de uso del paquete java.util.concurrent.
El patrón más importante es lock en finally. Independientemente de si la sección crítica se completa con éxito o lanza una excepción, el bloqueo debe liberarse. Esto garantiza que otros hilos no queden bloqueados para siempre debido a un solo error. En Kotlin, este patrón se resuelve elegantemente mediante la extensión withLock.
La sección crítica debe ser lo más corta posible. Nunca realice E/S, solicitudes de red o cálculos prolongados dentro de un bloqueo. Si necesita leer datos de un servidor, obténgalos primero y luego adquiera el bloqueo solo para actualizar el estado compartido. Esto reduce la contención y mejora el rendimiento del sistema.
Para prevenir deadlocks al trabajar con múltiples bloqueos, establezca un orden global de adquisición en todo el proyecto. Si primero se adquiere lockA y luego lockB, cualquier secuencia inversa debe prohibirse mediante las reglas de revisión de código. Use analizadores estáticos como SpotBugs e IntelliJ Inspections para verificación automática.
Preguntas Frecuentes
Lock es una interfaz explícita con soporte de tiempo de espera y espera interrumpible. synchronized adquiere y libera automáticamente el monitor, pero no permite usar tryLock, lockInterruptibly ni múltiples Conditions. Lock es más flexible, pero requiere liberación manual en finally.
Un bloqueo justo garantiza orden FIFO: el hilo que ha estado esperando más tiempo recibe el bloqueo primero. Un bloqueo injusto puede conceder acceso a un nuevo hilo por delante de los que esperan, lo que aumenta el rendimiento pero puede causar inanición de los hilos en espera.
Siga un orden fijo para adquirir todos los bloqueos, use tryLock con tiempo de espera en lugar de lock() incondicional, y minimice el número de bloqueos mantenidos simultáneamente. El uso de estructuras de datos Lock-Free también reduce el riesgo de deadlock.
Condition es el análogo de wait/notify para Lock, que permite múltiples colas de espera independientes. Cada llamada a newCondition() crea una cola separada, proporcionando un control más preciso sobre la activación de hilos en comparación con la cola única de synchronized.
Para Android con corrutinas, use Mutex de kotlinx.coroutines — suspende la corrutina en lugar de bloquear el hilo. Para iOS con Swift 5.7+, son preferibles los actores, que sincronizan automáticamente el acceso al estado. Reserve ReentrantLock para código heredado y escenarios de bajo nivel.
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