Guía de java.util.concurrent.Locks

1. Información general

En pocas palabras, un candado es un mecanismo de sincronización de subprocesos más flexible y sofisticado que el bloque sincronizado estándar .

La interfaz de bloqueo existe desde Java 1.5. Está definido dentro del paquete java.util.concurrent.lock y proporciona amplias operaciones de bloqueo.

En este artículo, exploraremos diferentes implementaciones de la interfaz Lock y sus aplicaciones.

2. Diferencias entre bloqueo y bloque sincronizado

Existen pocas diferencias entre el uso de bloques sincronizados y el uso de API de bloqueo :

  • A sincronizada bloque está totalmente contenida dentro de un método - podemos tener Lock de API de bloqueo () y de desbloqueo () operación en métodos separados
  • Un bloque sincronizado no admite la equidad, cualquier hilo puede adquirir el bloqueo una vez liberado, no se puede especificar ninguna preferencia. Podemos lograr la equidad dentro de las API de bloqueo especificando la propiedad de equidad . Se asegura de que el hilo de espera más largo tenga acceso al bloqueo
  • Un hilo se bloquea si no puede acceder al bloque sincronizado . La API de bloqueo proporciona el método tryLock () . El hilo adquiere bloqueo solo si está disponible y no está retenido por ningún otro hilo. Esto reduce el tiempo de bloqueo del hilo esperando el bloqueo.
  • Un hilo que está en estado de "espera" para adquirir el acceso al bloque sincronizado , no puede ser interrumpido. La API de bloqueo proporciona un método lockInterruptiblemente () que se puede utilizar para interrumpir el hilo cuando está esperando el bloqueo

3. Bloquear API

Echemos un vistazo a los métodos en la interfaz de bloqueo :

  • void lock () : adquiere el bloqueo si está disponible; si el candado no está disponible, un hilo se bloquea hasta que se suelta el candado
  • void lockInterruptbly () : esto es similar al lock (), pero permite que el hilo bloqueado se interrumpa y reanude la ejecución a través de una java.lang.InterruptedException lanzada
  • boolean tryLock () : esta es una versión sin bloqueo delmétodo lock () ; intenta adquirir el bloqueo de inmediato, devuelve verdadero si el bloqueo tiene éxito
  • boolean tryLock (tiempo de espera largo, TimeUnit timeUnit) : esto es similar a tryLock (), excepto que espera el tiempo de espera dado antes de dejar de intentar adquirir el bloqueo
  • void unlock () : desbloquea la instancia de Lock

Una instancia bloqueada siempre debe estar desbloqueada para evitar una condición de interbloqueo. Un bloque de código recomendado para usar el bloqueo debe contener un try / catch y finalmente bloquear:

Lock lock = ...; lock.lock(); try { // access to the shared resource } finally { lock.unlock(); }

Además de la interfaz de bloqueo , tenemos una interfaz ReadWriteLock que mantiene un par de bloqueos, uno para operaciones de solo lectura y otro para la operación de escritura. El bloqueo de lectura puede ser mantenido simultáneamente por varios subprocesos siempre que no haya escritura.

ReadWriteLock declara métodos para adquirir bloqueos de lectura o escritura:

  • Lock readLock () : devuelve el bloqueo que se usa para leer
  • Lock writeLock () : devuelve el bloqueo que se usa para escribir

4. Bloquear implementaciones

4.1. ReentrantLock

La clase ReentrantLock implementa la interfaz Lock . Ofrece la misma semántica de simultaneidad y memoria que el bloqueo de monitor implícito al que se accede mediante métodos y declaraciones sincronizados , con capacidades ampliadas.

Veamos cómo podemos usar ReenrtantLock para la sincronización:

public class SharedObject { //... ReentrantLock lock = new ReentrantLock(); int counter = 0; public void perform() { lock.lock(); try { // Critical section here count++; } finally { lock.unlock(); } } //... }

Necesitamos asegurarnos de que estamos ajustando las llamadas de bloqueo () y desbloqueo () en el bloque intentar-finalmente para evitar situaciones de interbloqueo.

Veamos cómo funciona tryLock () :

public void performTryLock(){ //... boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS); if(isLockAcquired) { try { //Critical section here } finally { lock.unlock(); } } //... } 

En este caso, el hilo que llama a tryLock () esperará un segundo y dejará de esperar si el bloqueo no está disponible.

4.2. ReentrantReadWriteLock

La clase ReentrantReadWriteLock implementa la interfaz ReadWriteLock .

Veamos las reglas para adquirir ReadLock o WriteLock mediante un hilo:

  • Bloqueo de lectura : si ningún subproceso adquirió el bloqueo de escritura o lo solicitó, entonces varios subprocesos pueden adquirir el bloqueo de lectura
  • Bloqueo de escritura : si no hay subprocesos leyendo o escribiendo, solo un subproceso puede adquirir el bloqueo de escritura

Veamos cómo hacer uso de ReadWriteLock :

public class SynchronizedHashMapWithReadWriteLock { Map syncHashMap = new HashMap(); ReadWriteLock lock = new ReentrantReadWriteLock(); // ... Lock writeLock = lock.writeLock(); public void put(String key, String value) { try { writeLock.lock(); syncHashMap.put(key, value); } finally { writeLock.unlock(); } } ... public String remove(String key){ try { writeLock.lock(); return syncHashMap.remove(key); } finally { writeLock.unlock(); } } //... }

Para ambos métodos de escritura, necesitamos rodear la sección crítica con el bloqueo de escritura, solo un hilo puede acceder a él:

Lock readLock = lock.readLock(); //... public String get(String key){ try { readLock.lock(); return syncHashMap.get(key); } finally { readLock.unlock(); } } public boolean containsKey(String key) { try { readLock.lock(); return syncHashMap.containsKey(key); } finally { readLock.unlock(); } }

Para ambos métodos de lectura, debemos rodear la sección crítica con el bloqueo de lectura. Varios subprocesos pueden obtener acceso a esta sección si no hay ninguna operación de escritura en curso.

4.3. SelladoBloqueo

StampedLock se introdujo en Java 8. También admite bloqueos de lectura y escritura. Sin embargo, los métodos de adquisición de candados devuelven un sello que se usa para liberar un candado o para verificar si el candado sigue siendo válido:

public class StampedLockDemo { Map map = new HashMap(); private StampedLock lock = new StampedLock(); public void put(String key, String value){ long stamp = lock.writeLock(); try { map.put(key, value); } finally { lock.unlockWrite(stamp); } } public String get(String key) throws InterruptedException { long stamp = lock.readLock(); try { return map.get(key); } finally { lock.unlockRead(stamp); } } }

Otra característica proporcionada por StampedLock es el bloqueo optimista. La mayoría de las veces, las operaciones de lectura no necesitan esperar a que se complete la operación de escritura y, como resultado de esto, no se requiere el bloqueo de lectura completo.

En su lugar, podemos actualizar para leer bloqueo:

public String readWithOptimisticLock(String key) { long stamp = lock.tryOptimisticRead(); String value = map.get(key); if(!lock.validate(stamp)) { stamp = lock.readLock(); try { return map.get(key); } finally { lock.unlock(stamp); } } return value; }

5. Working With Conditions

The Condition class provides the ability for a thread to wait for some condition to occur while executing the critical section.

This can occur when a thread acquires the access to the critical section but doesn't have the necessary condition to perform its operation. For example, a reader thread can get access to the lock of a shared queue, which still doesn't have any data to consume.

Traditionally Java provides wait(), notify() and notifyAll() methods for thread intercommunication. Conditions have similar mechanisms, but in addition, we can specify multiple conditions:

public class ReentrantLockWithCondition { Stack stack = new Stack(); int CAPACITY = 5; ReentrantLock lock = new ReentrantLock(); Condition stackEmptyCondition = lock.newCondition(); Condition stackFullCondition = lock.newCondition(); public void pushToStack(String item){ try { lock.lock(); while(stack.size() == CAPACITY) { stackFullCondition.await(); } stack.push(item); stackEmptyCondition.signalAll(); } finally { lock.unlock(); } } public String popFromStack() { try { lock.lock(); while(stack.size() == 0) { stackEmptyCondition.await(); } return stack.pop(); } finally { stackFullCondition.signalAll(); lock.unlock(); } } }

6. Conclusion

En este artículo, hemos visto diferentes implementaciones de la interfaz Lock y la clase StampedLock recién introducida . También exploramos cómo podemos hacer uso de la clase Condition para trabajar con múltiples condiciones.

El código completo de este tutorial está disponible en GitHub.