Java ReentrantReadWriteLocks: ¿cómo adquirir de forma segura el locking de escritura?

Estoy utilizando en mi código en este momento un ReentrantReadWriteLock para sincronizar el acceso a través de una estructura similar a un árbol. Esta estructura es grande y es leída por muchos hilos a la vez con modificaciones ocasionales en partes pequeñas, por lo que parece ajustarse bien a la expresión de lectura y escritura. Entiendo que con esta clase en particular, uno no puede elevar un locking de lectura a un locking de escritura, por lo que según Javadocs uno debe liberar el locking de lectura antes de obtener el locking de escritura. He utilizado este patrón con éxito en contextos sin reentrada antes.

Sin embargo, lo que estoy descubriendo es que no puedo adquirir confiablemente el locking de escritura sin bloquear para siempre. Como el locking de lectura es reentrante y en realidad lo estoy usando como tal, el código simple

lock.getReadLock().unlock(); lock.getWriteLock().lock() 

puede bloquear si he adquirido el readlock reentrante. Cada llamada para desbloquear solo reduce el conteo de espera, y el locking solo se libera cuando el conteo de espera llega a cero.

EDITAR para aclarar esto, ya que no creo que lo haya explicado demasiado bien al principio: soy consciente de que no existe una escalada de locking incorporada en esta clase, y que simplemente tengo que liberar el locking de lectura y obtener el locking de escritura. Mi problema es / fue que independientemente de lo que estén haciendo otros subprocesos, al llamar a getReadLock().unlock() puede que no libere la retención de este thread en el locking si lo adquirió de manera reentrante, en cuyo caso la llamada a getWriteLock().lock() bloqueará para siempre ya que este hilo todavía tiene una retención en el locking de lectura y por lo tanto se bloquea a sí mismo.

Por ejemplo, este fragmento de código nunca llegará a la instrucción println, incluso cuando se ejecuta singlethreaded sin ningún otro subproceso que acceda al locking:

 final ReadWriteLock lock = new ReentrantReadWriteLock(); lock.getReadLock().lock(); // In real code we would go call other methods that end up calling back and // thus locking again lock.getReadLock().lock(); // Now we do some stuff and realise we need to write so try to escalate the // lock as per the Javadocs and the above description lock.getReadLock().unlock(); // Does not actually release the lock lock.getWriteLock().lock(); // Blocks as some thread (this one!) holds read lock System.out.println("Will never get here"); 

Entonces pregunto, ¿hay un buen idioma para manejar esta situación? Específicamente, cuando un hilo que contiene un locking de lectura (posiblemente de manera reentrante) descubre que necesita escribir algo y, por lo tanto, quiere “suspender” su propio locking de lectura para recoger el locking de escritura (bloqueando según se requiera en otros hilos a suelte sus presas en el locking de lectura), y luego “levante” su agarre en el locking de lectura en el mismo estado después?

Dado que esta implementación ReadWriteLock fue diseñada específicamente para ser reentrada, seguramente hay alguna manera sensata de elevar un locking de lectura a un locking de escritura cuando los lockings pueden adquirirse de manera retrógrada. Esta es la parte crítica que significa que el enfoque ingenuo no funciona.

He progresado un poco en esto. Al declarar explícitamente la variable de locking como ReentrantReadWriteLock lugar de simplemente ReadWriteLock (menos que ideal, pero probablemente un mal necesario en este caso) puedo llamar al método getReadHoldCount() . Esto me permite obtener el número de retenciones para el subproceso actual y, por lo tanto, puedo liberar el readlock muchas veces (y volver a adquirir el mismo número después). Entonces esto funciona, como lo muestra una prueba rápida y sucia:

 final int holdCount = lock.getReadHoldCount(); for (int i = 0; i < holdCount; i++) { lock.readLock().unlock(); } lock.writeLock().lock(); try { // Perform modifications } finally { // Downgrade by reacquiring read lock before releasing write lock for (int i = 0; i < holdCount; i++) { lock.readLock().lock(); } lock.writeLock().unlock(); } 

Aún así, ¿va a ser esto lo mejor que puedo hacer? No se siente muy elegante, y sigo esperando que haya una manera de manejar esto de una manera menos “manual”.

Lo que quieres hacer debería ser posible. El problema es que Java no proporciona una implementación que pueda actualizar lockings de lectura para escribir lockings. Específicamente, javadoc ReentrantReadWriteLock dice que no permite una actualización desde el locking de lectura al locking de escritura.

En cualquier caso, Jakob Jenkov describe cómo implementarlo. Ver http://tutorials.jenkov.com/java-concurrency/read-write-locks.html#upgrade para más detalles.

Por qué es necesario actualizar las cerraduras de lectura a escritura

Una actualización del locking de lectura a escritura es válida (a pesar de las afirmaciones de lo contrario en otras respuestas). Puede producirse un interlocking, por lo que parte de la implementación es un código para reconocer interlockings y romperlos lanzando una excepción en un hilo para romper el punto muerto. Eso significa que, como parte de su transacción, debe manejar la DeadlockException, por ejemplo, haciendo el trabajo nuevamente. Un patrón típico es:

 boolean repeat; do { repeat = false; try { readSomeStuff(); writeSomeStuff(); maybeReadSomeMoreStuff(); } catch (DeadlockException) { repeat = true; } } while (repeat); 

Sin esta capacidad, la única forma de implementar una transacción serializable que lea un montón de datos consistentemente y luego escriba algo según lo que se leyó es anticipar que la escritura será necesaria antes de comenzar y, por lo tanto, obtener lockings ESCRIBIR en todos los datos que son Lea antes de escribir lo que debe escribirse. Este es un KLUDGE que utiliza Oracle (SELECCIONAR PARA ACTUALIZAR …). Además, en realidad reduce la concurrencia porque nadie más puede leer o escribir ninguno de los datos mientras se ejecuta la transacción.

En particular, liberar el locking de lectura antes de obtener el locking de escritura producirá resultados inconsistentes. Considerar:

 int x = someMethod(); y.writeLock().lock(); y.setValue(x); y.writeLock().unlock(); 

Debe saber si algún Método () o cualquier método al que llama crea un locking de lectura reentrante en y! Supongamos que sabes que sí. Luego, si sueltas el locking de lectura primero:

 int x = someMethod(); y.readLock().unlock(); // problem here! y.writeLock().lock(); y.setValue(x); y.writeLock().unlock(); 

otro hilo puede cambiar y después de que suelte su locking de lectura, y antes de que obtenga el locking de escritura en él. Entonces, el valor de y no será igual a x.

Código de prueba: actualización de un locking de lectura a bloques de locking de escritura:

 import java.util.*; import java.util.concurrent.locks.*; public class UpgradeTest { public static void main(String[] args) { System.out.println("read to write test"); ReadWriteLock lock = new ReentrantReadWriteLock(); lock.readLock().lock(); // get our own read lock lock.writeLock().lock(); // upgrade to write lock System.out.println("passed"); } } 

Salida usando Java 1.6:

 read to write test  

Esta es una vieja pregunta, pero aquí hay una solución al problema y algo de información de fondo.

Como han señalado otros, un locking clásico de lector-escritor (como el JDK ReentrantReadWriteLock ) no admite inherentemente la actualización de un locking de lectura a un locking de escritura, porque al hacerlo es susceptible de interlocking.

Si necesita adquirir de forma segura un locking de escritura sin antes liberar un locking de lectura, existe una mejor alternativa: echar un vistazo a un locking de lectura-escritura- actualización en su lugar.

Escribí ReentrantReadWrite_Update_Lock y lo lancé como código abierto bajo una licencia Apache 2.0 aquí . También publiqué detalles del enfoque de la lista de correo de interés concurrente de JSR166, y el enfoque sobrevivió a un examen exhaustivo por parte de los miembros de esa lista.

El enfoque es bastante simple, y como mencioné en concurrency-interest, la idea no es completamente nueva, ya que se discutió en la lista de correo del kernel de Linux al menos hasta el año 2000. También ReaderWriterLockSlim de la plataforma .Net admite la actualización de locking además. Así que efectivamente este concepto simplemente no se había implementado en Java (AFAICT) hasta ahora.

La idea es proporcionar un locking de actualización además del locking de lectura y el locking de escritura . Un locking de actualización es un tipo intermedio de locking entre un locking de lectura y un locking de escritura. Al igual que el locking de escritura, solo un hilo puede adquirir un locking de actualización a la vez. Pero al igual que un locking de lectura, permite el acceso de lectura al hilo que lo contiene, y al mismo tiempo a otros hilos que tienen lockings de lectura regulares. La característica clave es que el locking de actualización se puede actualizar desde su estado de solo lectura, a un locking de escritura, y esto no es susceptible de interlocking porque solo un hilo puede contener un locking de actualización y estar en posición de actualizar a la vez.

Esto es compatible con la actualización de locking y, además, es más eficiente que un locking convencional de lector y escritor en aplicaciones con patrones de acceso de lectura antes de escritura , ya que bloquea los hilos de lectura durante periodos de tiempo más cortos.

El uso de ejemplo se proporciona en el sitio . La biblioteca tiene una cobertura de prueba del 100% y se encuentra en el centro de Maven.

Lo que estás tratando de hacer simplemente no es posible de esta manera.

No puede tener un locking de lectura / escritura que puede actualizar de lectura a escritura sin problemas. Ejemplo:

 void test() { lock.readLock().lock(); ... if ( ... ) { lock.writeLock.lock(); ... lock.writeLock.unlock(); } lock.readLock().unlock(); } 

Ahora supongamos que dos hilos entrarían en esa función. (Y usted está asumiendo la concurrencia, ¿verdad? De lo contrario, no le importarían los lockings en primer lugar …)

Supongamos que ambos hilos comenzarán al mismo tiempo y se ejecutarán igualmente rápido . Eso significa que ambos obtendrían un locking de lectura, lo cual es perfectamente legal. Sin embargo, ambos intentarán adquirir el locking de escritura, que NINGUNO de ellos obtendrá jamás: ¡los otros hilos restantes tienen un locking de lectura!

Los lockings que permiten la actualización de lockings de lectura para escribir lockings son propensos a lockings por definición. Lo siento, pero debes modificar tu enfoque.

Lo que estás buscando es una actualización de locking, y no es posible (al menos no atómicamente) usando el ReentrantReadWriteLock java.concurrent estándar. Su mejor opción es desbloquear / bloquear, y luego verificar que nadie haya hecho modificaciones entre ellos.

Lo que estás intentando hacer, forzando a todos los lockings de lectura a salir del camino, no es una muy buena idea. Los lockings de lectura están ahí por una razón, que no debe escribir. 🙂

EDITAR:
Como señaló Ran Biron, si su problema es la inanición (los lockings de lectura se establecen y se liberan todo el tiempo, nunca cayendo a cero), podría intentar usar la cola justa. Pero su pregunta no sonó como si este fuera su problema?

EDICION 2:
Ahora veo su problema, en realidad adquirió varios lockings de lectura en la stack y desea convertirlos en un locking de escritura (actualización). Esto es, de hecho, imposible con la implementación de JDK, ya que no hace un seguimiento de los propietarios del locking de lectura. Podría haber otros que tengan lockings de lectura que no verías, y no tiene idea de cuántos de los lockings de lectura pertenecen a tu hilo, sin mencionar tu stack de llamadas actual (es decir, tu ciclo está matando a todos los lockings de lectura, no solo el suyo, entonces su locking de escritura no esperará a que los lectores concurrentes terminen, y terminará con un desastre en sus manos)

De hecho, he tenido un problema similar y terminé escribiendo mi propia cuenta de quién sabe qué cerraduras de lectura y actualizando a lockings de escritura. Aunque esto también era un tipo de locking de lectura / escritura Copy-on-Write (lo que permitía a un escritor a lo largo de los lectores), por lo que era un poco diferente.

Java 8 ahora tiene java.util.concurrent.locks.StampedLock con una tryConvertToWriteLock(long)

Más información en http://www.javaspecialists.eu/archive/Issue215.html

¿Qué tal esto algo así?

 class CachedData { Object data; volatile boolean cacheValid; private class MyRWLock { private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public synchronized void getReadLock() { rwl.readLock().lock(); } public synchronized void upgradeToWriteLock() { rwl.readLock().unlock(); rwl.writeLock().lock(); } public synchronized void downgradeToReadLock() { rwl.writeLock().unlock(); rwl.readLock().lock(); } public synchronized void dropReadLock() { rwl.readLock().unlock(); } } private MyRWLock myRWLock = new MyRWLock(); void processCachedData() { myRWLock.getReadLock(); try { if (!cacheValid) { myRWLock.upgradeToWriteLock(); try { // Recheck state because another thread might have acquired write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } } finally { myRWLock.downgradeToReadLock(); } } use(data); } finally { myRWLock.dropReadLock(); } } } 

Supongo que el ReentrantLock está motivado por un recorrido recursivo del árbol:

 public void doSomething(Node node) { // Acquire reentrant lock ... // Do something, possibly acquire write lock for (Node child : node.childs) { doSomething(child); } // Release reentrant lock } 

¿No puedes refactorizar tu código para mover el manejo de la cerradura fuera de la recursión?

 public void doSomething(Node node) { // Acquire NON-reentrant read lock recurseDoSomething(node); // Release NON-reentrant read lock } private void recurseDoSomething(Node node) { ... // Do something, possibly acquire write lock for (Node child : node.childs) { recurseDoSomething(child); } } 

Entonces, ¿estamos esperando que Java incremente el conteo de semáforos de lectura solo si este hilo aún no ha contribuido al readHoldCount? Lo que significa que a diferencia de mantener un ThreadLocal readholdCount de tipo int, debe mantener ThreadLocal Set de tipo Integer (manteniendo el hasCode del hilo actual). Si esto está bien, sugeriría (al menos por ahora) que no llame a varias llamadas de lectura dentro de la misma clase, sino que use una bandera para verificar, ya sea que el locking de lectura ya haya sido obtenido por el objeto actual o no.

 private volatile boolean alreadyLockedForReading = false; public void lockForReading(Lock readLock){ if(!alreadyLockedForReading){ lock.getReadLock().lock(); } } 

OP: simplemente desbloquee tantas veces como ingrese al candado, así de simple:

 boolean needWrite = false; readLock.lock() try{ needWrite = checkState(); }finally{ readLock().unlock() } //the state is free to change right here, but not likely //see who has handled it under the write lock, if need be if (needWrite){ writeLock().lock(); try{ if (checkState()){//check again under the exclusive write lock //modify state } }finally{ writeLock.unlock() } } 

en el locking de escritura, ya que cualquier progtwig concurrente de autorrespeto verifica el estado necesario.

HoldCount no se debe usar más allá de la detección de errores / monitores / fallas rápidas.

Se encuentra en la documentación de ReentrantReadWriteLock . Dice claramente que los hilos de lectura nunca tendrán éxito cuando intente adquirir un locking de escritura. Lo que intentas lograr simplemente no es compatible. Debe liberar el locking de lectura antes de adquirir el locking de escritura. Una rebaja aún es posible.

Reentrada

Este locking permite a los lectores y escritores volver a adquirir lockings de lectura o escritura con el estilo de {@link ReentrantLock}. Los lectores no reentrantes no están permitidos hasta que se hayan liberado todos los lockings de escritura retenidos por el hilo de escritura.

Además, un escritor puede adquirir el locking de lectura, pero no viceversa. Entre otras aplicaciones, la reentrada puede ser útil cuando se mantienen lockings de escritura durante llamadas o devoluciones de llamadas a métodos que realizan lecturas bajo lockings de lectura. Si un lector intenta adquirir el locking de escritura, nunca tendrá éxito.

Uso de muestra de la fuente anterior:

  class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); } } 

Use la bandera “justa” en ReentrantReadWriteLock. “justo” significa que las solicitudes de locking se entregan por orden de llegada. Puede experimentar una depredación del rendimiento, ya que cuando se emite una solicitud de “escritura”, todas las solicitudes de “lectura” posteriores se bloquearán, incluso si se hubieran podido servir mientras los lockings de lectura preexistentes todavía están bloqueados.