it-swarm.com.de

Java ReentrantReadWriteLocks - wie man Schreibsperre sicher erhält?

Ich verwende in meinem Code im Moment ein ReentrantReadWriteLock , um den Zugriff über eine baumartige Struktur zu synchronisieren. Diese Struktur ist groß und kann von vielen Threads gleichzeitig gelesen werden, wobei kleine Teile davon gelegentlich modifiziert werden. Sie scheint also gut in das Lese-Schreib-Idiom zu passen. Ich verstehe, dass mit dieser bestimmten Klasse eine Lesesperre nicht zu einer Schreibsperre aufgehoben werden kann. Daher muss die Lesesperre für Javadocs vor dem Erhalt der Schreibsperre freigegeben werden. Ich habe dieses Muster bereits in nicht wiedereintretenden Kontexten erfolgreich verwendet.

Was ich jedoch feststelle, ist, dass ich die Schreibsperre nicht zuverlässig erwerben kann, ohne für immer zu blockieren. Da die Lesesperre wiedereintrittsfähig ist und ich damit eigentlich den einfachen Code verwende

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

kann blockieren, wenn ich den Readlock wiedereintretend erworben habe. Jeder Aufruf zum Entsperren verringert lediglich die Haltezählung, und die Sperre wird erst dann tatsächlich aufgehoben, wenn die Haltezählung Null erreicht. 

EDIT, um dies zu klären, da ich glaube, dass ich es anfangs nicht gut erklärt habe - ich weiß, dass es in dieser Klasse keine eingebaute Sperreneskalation gibt, und dass ich einfach die Lesesperre aufheben muss und erhalte die Schreibsperre. Mein Problem ist/war, dass unabhängig von den anderen Threads der Aufruf von getReadLock().unlock() die Sperrung von this thread nicht wirklich freigibt, wenn er sie wiedereintretend erworben hat. In diesem Fall wird der Aufruf von getWriteLock().lock() für immer blockiert Thread hat immer noch die Lesesperre im Griff und blockiert sich somit selbst.

Dieses Code-Snippet kann beispielsweise die Anweisung println nicht erreichen, selbst wenn Singlethread ausgeführt wird, ohne dass andere Threads auf die Sperre zugreifen:

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");

Also frage ich, gibt es eine schöne Sprache, um mit dieser Situation umzugehen? Insbesondere wenn ein Thread, der eine Lesesperre hält (möglicherweise wiedereintrittsbedingt), entdeckt, dass er etwas schreiben muss, und möchte daher seine eigene Lesesperre "aufheben", um die Schreibsperre aufzuheben (Blockieren bei anderen Threads) Lösen Sie ihre Haltepunkte an der Lesesperre) und "heben" Sie anschließend die Lesesperre im gleichen Zustand wieder auf.

Da diese ReadWriteLock-Implementierung speziell dafür konzipiert wurde, wiedereintrittsfähig zu sein, gibt es sicherlich eine vernünftige Möglichkeit, eine Lesesperre zu einer Schreibsperre aufzuheben, wenn die Sperren wiedereintrittsfähig gemacht werden können. Dies ist der kritische Teil, der besagt, dass der naive Ansatz nicht funktioniert.

60
Andrzej Doyle

Ich habe ein bisschen Fortschritte gemacht. Indem ich die lock-Variable explizit als ReentrantReadWriteLock deklariert und nicht einfach als ReadWriteLock (nicht ideal, aber in diesem Fall wahrscheinlich ein notwendiges Übel), kann ich die getReadHoldCount() -Methode aufrufen. Dadurch kann ich die Anzahl der Holds für den aktuellen Thread ermitteln, und daher kann ich den Lesevorgang so oft freigeben (und danach dieselbe Anzahl erneut erwerben). Das funktioniert also, wie ein schneller und schmutziger Test zeigt:

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();
}

Aber ist dies das Beste, was ich tun kann? Es fühlt sich nicht sehr elegant an und ich hoffe immer noch, dass es einen Weg gibt, dies auf eine weniger "manuelle" Art und Weise zu handhaben.

27
Andrzej Doyle

Was Sie tun möchten, sollte möglich sein. Das Problem ist, dass Java keine Implementierung bereitstellt, die Lesesperren auf Schreibsperren aufrüsten kann. Insbesondere sagt der Javadoc ReentrantReadWriteLock, dass ein Upgrade von Lesesperren auf Schreibsperren nicht zulässig ist. 

In jedem Fall beschreibt Jakob Jenkov, wie er implementiert wird. Siehe http://tutorials.jenkov.com/Java-concurrency/read-write-locks.html#upgrade für Einzelheiten. 

Warum ein Upgrade von Read-to-Write-Sperren erforderlich ist

Ein Upgrade von Lese- auf Schreibsperre ist gültig (trotz gegenteiliger Behauptungen in anderen Antworten). Es kann zu einem Deadlock kommen. Teil der Implementierung ist Code, um Deadlocks zu erkennen und zu brechen, indem eine Ausnahme in einem Thread ausgelöst wird, um den Deadlock zu lösen. Das bedeutet, dass Sie als Teil Ihrer Transaktion die DeadlockException behandeln müssen, z. B. indem Sie die Arbeit erneut ausführen. Ein typisches Muster ist:

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

Ohne diese Fähigkeit ist die einzige Möglichkeit, eine serialisierbare Transaktion zu implementieren, die eine Reihe von Daten konsistent liest und dann basierend auf dem Gelesenen etwas schreibt, darin zu erwarten, dass geschrieben werden muss, bevor Sie beginnen, und daher WRITE-Sperren für alle vorhandenen Daten erhalten Lesen Sie vor dem Schreiben, was geschrieben werden muss. Dies ist ein von Oracle verwendeter KLUDGE (SELECT FOR UPDATE ...). Darüber hinaus reduziert es tatsächlich die Parallelität, da niemand die Daten während der Transaktion lesen oder schreiben kann!

Insbesondere die Freigabe der Lesesperre vor dem Erhalt der Schreibsperre führt zu inkonsistenten Ergebnissen. Erwägen:

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

Sie müssen wissen, ob someMethod () oder eine von ihr aufgerufene Methode eine wiedereintrittsfähige Lesesperre für y erstellt! Angenommen, Sie wissen es. Dann, wenn Sie die Lesesperre zuerst aufheben:

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

ein anderer Thread kann y ändern, nachdem Sie die Lesesperre freigegeben haben und bevor Sie die Schreibsperre erhalten. Der Wert von y ist also nicht gleich x.

Testcode: Aktualisieren einer Lesesperre auf Schreibsperrenblöcke:

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");
    }

}

Ausgabe mit Java 1.6:

read to write test
<blocks indefinitely>
25
Bob

Dies ist eine alte Frage, aber hier sind sowohl eine Lösung für das Problem als auch einige Hintergrundinformationen.

Wie bereits erwähnt, unterstützt ein klassisches Lese-Schreib-Sperre (wie das JDK ReentrantReadWriteLock ) inhärent kein Upgrade einer Lesesperre auf eine Schreibsperre, da dies zu einem Deadlock führen kann .

Wenn Sie eine Schreibsperre sicher erwerben möchten, ohne zuvor eine Lesesperre aufzuheben, gibt es eine bessere Alternative: Schauen Sie sich stattdessen eine Sperre read-write -update an.

Ich habe ein ReentrantReadWrite_Update_Lock geschrieben und als Open Source unter einer Apache 2.0-Lizenz hier veröffentlicht. Ich habe auch Details zu dem Ansatz der JSR166 Concurrency-Interest-Mailingliste veröffentlicht, und der Ansatz hat einige Hin und Her-Überprüfungen durch die Mitglieder dieser Liste überstanden.

Die Herangehensweise ist ziemlich einfach, und wie ich bereits im Zusammenhang mit Parallelität erwähnte, ist die Idee nicht völlig neu, da sie zumindest im Jahr 2000 auf der Linux-Kernel-Mailingliste diskutiert wurde. Auch die ReaderWriterLockSlim unterstützt auch das Sperrupgrade. Dieses Konzept wurde so gut wie noch nicht auf Java (AFAICT) implementiert.

Die Idee ist, zusätzlich zu der Sperre read und der Sperre write eine Sperre update bereitzustellen. Eine Aktualisierungssperre ist eine Zwischentypsperre zwischen einer Lesesperre und einer Schreibsperre. Wie bei der Schreibsperre kann immer nur ein Thread eine Aktualisierungssperre erwerben. Aber wie eine Lesesperre ermöglicht sie Lesezugriff auf den Thread, der sie enthält, und gleichzeitig auf andere Threads, die reguläre Lesesperren enthalten. Die Schlüsselfunktion besteht darin, dass die Aktualisierungssperre von ihrem Nur-Lese-Status auf eine Schreibsperre aktualisiert werden kann. Dies ist nicht anfällig für Deadlocks, da nur ein Thread eine Aktualisierungssperre halten kann und gleichzeitig in der Lage ist, ein Upgrade durchzuführen.

Dies unterstützt das Aktualisieren von Sperren und ist außerdem effizienter als eine herkömmliche Lese-Schreib-Sperre in Anwendungen mit Zugriffsmustern read-before-write, da das Lesen von Threads für kürzere Zeitspannen blockiert wird.

Die Verwendung von Beispielen wird auf der Site bereitgestellt. Die Bibliothek hat eine Testabdeckung von 100% und befindet sich in Maven Central.

19
npgall

Was Sie versuchen, ist auf diese Weise einfach nicht möglich.

Sie können keine Lese-/Schreibsperre haben, die Sie ohne Probleme von Lesen auf Schreiben aktualisieren können. Beispiel:

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

Nehmen wir an, zwei Threads würden diese Funktion betreten. (Und Sie gehen davon aus, dass Parallelität besteht, oder? Ansonsten würden Sie sich überhaupt nicht für Schlösser interessieren ...)

Nehmen wir an, beide Threads würden zur gleichen Zeit starten und gleich schnell laufen. Das würde bedeuten, dass beide eine Lesesperre erwerben würden, was absolut legal ist. Dann würden jedoch beide versuchen, die Schreibsperre zu erhalten, die KEINE von ihnen jemals erhalten wird: Die jeweils anderen Threads halten eine Lesesperre!

Sperren, die ein Upgrade von Lesesperren auf Schreibsperren ermöglichen, neigen per Definition zu Deadlocks. Entschuldigung, aber Sie müssen Ihren Ansatz ändern.

8
Steffen Heil

Java 8 hat jetzt einen Java.util.concurrent.locks.StampedLock mit einer tryConvertToWriteLock(long)-API

Weitere Informationen unter http://www.javaspecialists.eu/archive/Issue215.html

4
qwerty

Was Sie suchen, ist ein Upgrade der Sperre und ist nicht möglich (zumindest nicht atomar), wenn Sie die standardmäßige Java.concurrent ReentrantReadWriteLock verwenden. Ihre beste Einstellung ist das Entsperren/Sperren. Überprüfen Sie dann, ob sich zwischen den Benutzern Änderungen vorgenommen haben.

Was Sie zu tun versuchen, alle Lesesperren aus dem Weg zu räumen, ist keine sehr gute Idee. Lesesperren gibt es aus einem Grund, aus dem Sie nicht schreiben sollten. :)

BEARBEITEN:
Wie Ran Biron feststellte, könnten Sie, wenn Ihr Problem Hunger ist (Lesesperren werden ständig gesetzt und aufgehoben werden, niemals auf Null fallen), versuchen, die Warteschlange zu verwenden. Aber deine Frage klang nicht so, dass es dein Problem war?

EDIT 2:
Ich sehe jetzt Ihr Problem, Sie haben tatsächlich mehrere Lesesperren auf dem Stack erworben und möchten diese in eine Schreibsperre konvertieren (Upgrade). Mit der JDK-Implementierung ist dies tatsächlich unmöglich, da die Besitzer der Lesesperre nicht nachverfolgt werden. Es könnte andere geben, die über Lesesperren verfügen, die Sie nicht sehen würden, und es hat keine Ahnung, wie viele Lesesperren zu Ihrem Thread gehören, ganz zu schweigen von Ihrem aktuellen Aufrufstapel (dh, Ihre Schleife tötet all Lesesperren, nicht nur Ihre eigenen, so dass Ihre Schreibsperre nicht auf das gleichzeitige Lesen von Lesern wartet, und Sie haben ein Durcheinander in den Händen.)

Ich hatte tatsächlich ein ähnliches Problem, und am Ende schrieb ich mein eigenes Schloss, um zu verfolgen, wer welche Lesesperren hat, und diese zu Schreibsperren zu aktualisieren. Obwohl es sich hierbei auch um eine Copy-on-Write-Schreib-/Lesesperre handelte (die es einem Schreiber ermöglichte, die Leser zu lesen), war es dennoch ein wenig anders.

4
falstro

Was ist mit so etwas? 

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();
        }
    }
}
2
Henry HO

Ich nehme an, die ReentrantLock wird durch eine rekursive Durchquerung des Baums motiviert:

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

Können Sie Ihren Code nicht umgestalten, um die Sperrenbehandlung außerhalb der Rekursion zu verschieben?

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);
  }
}
1
Olivier

zu OP: entsperren Sie einfach so oft, wie Sie das Schloss betreten haben, einfach so:

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()
  }
}

in der Schreibsperre als beliebiges Programm zur Selbsteinschätzung den erforderlichen Status prüfen.

HoldCount sollte nicht für Debug/Monitor/Fast-Fail-Erkennung verwendet werden.

1
xss

Erwarten wir also, dass Java die Anzahl der Lesesemaphore nur dann erhöht, wenn dieser Thread noch nicht zum readHoldCount beigetragen hat? Das bedeutet, dass im Gegensatz zu einem ThreadLocal-ReadholdCount-Typ vom Typ int der ThreadLocal-Satz vom Typ Integer beibehalten werden muss (der hasCode des aktuellen Threads wird beibehalten). Wenn dies in Ordnung ist, würde ich (zumindest für jetzt) ​​vorschlagen, nicht mehrere Leseaufrufe innerhalb derselben Klasse aufzurufen, sondern stattdessen mit einem Flag prüfen, ob die Lesesperre bereits vom aktuellen Objekt abgerufen wird oder nicht.

private volatile boolean alreadyLockedForReading = false;

public void lockForReading(Lock readLock){
   if(!alreadyLockedForReading){
      lock.getReadLock().lock();
   }
}
1
Vishal Angrish

Gefunden in der Dokumentation zu ReentrantReadWriteLock . Es wird deutlich, dass Reader-Threads niemals erfolgreich sein werden, wenn Sie versuchen, eine Schreibsperre zu erlangen. Was Sie erreichen möchten, wird einfach nicht unterstützt. Sie müssen die Lesesperre aufheben, bevor Sie die Schreibsperre erhalten. Ein Downgrade ist weiterhin möglich.

Wiedereintritt

Diese Sperre ermöglicht es Lesern und Schreibern, Lese- oder Schreibsperren im Stil einer {@link ReentrantLock} erneut abzurufen. Nicht wiedereintretende Leser sind erst zulässig, wenn alle Schreibsperren des Schreibthreads aufgehoben wurden.

Darüber hinaus kann ein Schreiber die Lesesperre erwerben, nicht jedoch umgekehrt. Unter anderen Anwendungen kann ein erneutes Eintreten nützlich sein, wenn Schreibsperren während Aufrufen oder Rückrufen von Methoden, die Lesevorgänge unter Lesesperren ausführen, gehalten werden. Wenn ein Leser versucht, die Schreibsperre zu erlangen, wird dies niemals erfolgreich sein.

Beispielnutzung aus der obigen Quelle:

 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();
   }
 }
0
phi