Wie kann festgestellt werden, ob ein Objekt gesperrt (synchronisiert) ist, um nicht in Java zu blockieren?

81

Ich habe einen Prozess A, der eine Tabelle im Speicher mit einer Reihe von Datensätzen enthält (recordA, recordB usw.)

Jetzt kann dieser Prozess viele Threads starten, die sich auf die Datensätze auswirken, und manchmal können zwei Threads versuchen, auf denselben Datensatz zuzugreifen - diese Situation muss abgelehnt werden. Insbesondere wenn ein Datensatz von einem Thread gesperrt wird, soll der andere Thread abgebrochen werden (ich möchte nicht blockieren oder warten).

Derzeit mache ich so etwas:

synchronized(record)
{
performOperation(record);
}

Aber das verursacht mir Probleme ... denn während Process1 die Operation ausführt, blockiert / wartet Process2, wenn es hereinkommt, die synchronisierte Anweisung und wenn Process1 beendet ist, führt es die Operation aus. Stattdessen möchte ich so etwas:

if (record is locked)
   return;

synchronized(record)
{
performOperation(record);
}

Irgendwelche Hinweise, wie dies erreicht werden kann? Jede Hilfe wäre sehr dankbar. Vielen Dank,

Shaitan00
quelle

Antworten:

87

Eine Sache zu beachten ist, dass in dem Moment, in dem Sie solche Informationen erhalten, diese veraltet sind. Mit anderen Worten, man könnte Ihnen sagen, dass niemand die Sperre hat, aber wenn Sie versuchen, sie zu erwerben, blockieren Sie sie, weil ein anderer Thread die Sperre zwischen der Prüfung und dem Versuch, sie zu erwerben, aufgehoben hat.

Brian hat Recht, darauf hinzuweisen Lock, aber ich denke, was Sie wirklich wollen, ist seine tryLockMethode:

Lock lock = new ReentrantLock();
......
if (lock.tryLock())
{
    // Got the lock
    try
    {
        // Process record
    }
    finally
    {
        // Make sure to unlock so that we don't cause a deadlock
        lock.unlock();
    }
}
else
{
    // Someone else had the lock, abort
}

Sie können auch tryLockmit einer gewissen Wartezeit anrufen. Sie können also versuchen, es für eine Zehntelsekunde zu erwerben, und dann abbrechen, wenn Sie es nicht bekommen (zum Beispiel).

(Ich finde es schade, dass die Java-API meines Wissens nicht die gleiche Funktionalität für das "eingebaute" Sperren bietet wie die MonitorKlasse in .NET. Andererseits gibt es viele andere Dinge, die ich auf beiden Plattformen beim Threading nicht mag - zum Beispiel jedes Objekt, das möglicherweise einen Monitor hat!)

Jon Skeet
quelle
Ja. Das ist ein guter Punkt. Ich habe den Beispielcode wörtlich genommen, während das Obige definitiv eine robustere Implementierung ist
Brian Agnew
5
Aber wie verwende ich eine Sperre pro Datensatz? Derzeit werden die Datensätze in einer HashTable of Records gespeichert. Benötige ich also eine passende Hashtable of Locks? Ich versuche sicherzustellen, dass ich die bestmögliche Parallelität habe. Wenn also ein Prozess auf recordC zugreifen möchte, sollte dies in Ordnung sein (wenn nur recordB gesperrt ist). Ich verwende ein globales LOCK. Dies entspricht im Wesentlichen dem Sperren der gesamten Hashtabelle. ... das macht irgendeinen Sinn?
Shaitan00
@ Shaitan00: Der einfachste Weg wäre, eine Sperre in der Aufzeichnung zu haben. Grundsätzlich möchten Sie jedem Datensatz eine Sperre zuordnen - setzen Sie sie also in das Objekt ein.
Jon Skeet
Offensichtlich :) Ich gehe davon aus, dass ich das Entsperren nicht verwalten muss? Das heißt, wenn es das {} von .tryLock () verlässt, wird es automatisch und sofort entsperrt, richtig?
Shaitan00
1
Sie müssen das Entsperren verwalten. Siehe das Tutorial, auf das ich in meiner Antwort verwiesen habe, und die Methode locker () im Block finally {}
Brian Agnew,
24

Sehen Sie sich die Lock- Objekte an, die in den Java 5-Parallelitätspaketen eingeführt wurden.

z.B

Lock lock = new ReentrantLock()
if (lock.tryLock()) {
   try {
      // do stuff using the lock...
   }
   finally {
      lock.unlock();
   }
}
   ...

Das ReentrantLock- Objekt macht im Wesentlichen dasselbe wie der herkömmliche synchronizedMechanismus, jedoch mit mehr Funktionalität.

BEARBEITEN: Wie Jon bemerkt hat, isLocked()sagt Ihnen die Methode zu diesem Zeitpunkt , und danach sind diese Informationen veraltet. Die tryLock () -Methode bietet einen zuverlässigeren Betrieb (beachten Sie, dass Sie dies auch mit einer Zeitüberschreitung verwenden können).

EDIT # 2: Das Beispiel enthält jetzt tryLock()/unlock()zur Verdeutlichung.

Brian Agnew
quelle
11

Ich habe Folgendes gefunden, mit dem wir Thread.holdsLock(Object obj)überprüfen können, ob ein Objekt gesperrt ist:

Gibt truegenau dann zurück, wenn der aktuelle Thread die Monitorsperre für das angegebene Objekt enthält.

Beachten Sie, dass Thread.holdsLock()zurückgegeben wird, falsewenn die Sperre von etwas gehalten wird und der aufrufende Thread nicht der Thread ist, der die Sperre hält.

Rahul Kulshreshtha
quelle
8

Der oben beschriebene Ansatz mit einem Sperrobjekt ist zwar der beste Weg, dies kann jedoch durchgeführt werden, wenn Sie mithilfe eines Monitors nach Sperren suchen müssen. Es wird jedoch eine Integritätswarnung angezeigt, da die Technik nicht auf Nicht-Oracle Java-VMs portierbar ist und in zukünftigen VM-Versionen möglicherweise nicht funktioniert, da es sich nicht um eine unterstützte öffentliche API handelt.

So geht's:

private static sun.misc.Unsafe getUnsafe() {
    try {
        Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

public void doSomething() {
  Object record = new Object();
  sun.misc.Unsafe unsafe = getUnsafe(); 
  if (unsafe.tryMonitorEnter(record)) {
    try {
      // record is locked - perform operations on it
    } finally {
      unsafe.monitorExit(record);
    }
  } else {
      // could not lock record
  }
}

Mein Rat wäre, diesen Ansatz nur zu verwenden, wenn Sie Ihren Code nicht umgestalten können, um dafür java.util.concurrent Lock-Objekte zu verwenden, und wenn Sie auf einer Oracle-VM ausgeführt werden.

Chris Wraith
quelle
12
@alestanis, ich stimme nicht zu. Hier lese ich diese Antworten heute und freue mich über alle Antworten / Kommentare, egal wann sie gegeben werden.
Tom
@ Tom Diese Antworten werden vom SO-System markiert, deshalb habe ich eine Nachricht hinterlassen. Sie werden sehen, wenn Sie mit der Überprüfung beginnen :)
Alestanis
2
@alestanis, ich finde das unglücklich - ich sehe oft alte Fragen, die akzeptierte Antworten haben, aber auch neuere, bessere Antworten, entweder weil sich die Technologie ändert oder weil die akzeptierte Antwort nicht ganz richtig war.
Tom
1
Zusätzlich dazu, dass dieser Thread alt ist und eine gute Antwort hat, ist diese Antwort hier nicht gut. Wenn Sie wirklich mit einem Monitor sperren möchten, können Sie Thread.holdsLock (Objekt) verwenden.
Tobias
3

Obwohl die Lock-Antworten sehr gut sind, dachte ich, ich würde eine Alternative mit einer anderen Datenstruktur veröffentlichen. Im Wesentlichen möchten Ihre verschiedenen Threads wissen, welche Datensätze gesperrt sind und welche nicht. Eine Möglichkeit, dies zu tun, besteht darin, die gesperrten Datensätze zu verfolgen und sicherzustellen, dass die Datenstruktur über die richtigen atomaren Operationen zum Hinzufügen von Datensätzen zum gesperrten Satz verfügt.

Ich werde CopyOnWriteArrayList als Beispiel verwenden, da es zur Veranschaulichung weniger "magisch" ist. CopyOnWriteArraySet ist eine geeignetere Struktur. Wenn im Durchschnitt viele, viele Datensätze gleichzeitig gesperrt sind, kann dies Auswirkungen auf die Leistung dieser Implementierungen haben. Ein ordnungsgemäß synchronisiertes HashSet würde ebenfalls funktionieren und die Sperren sind kurz.

Grundsätzlich würde der Verwendungscode folgendermaßen aussehen:

CopyOnWriteArrayList<Record> lockedRecords = ....
...
if (!lockedRecords.addIfAbsent(record))
    return; // didn't get the lock, record is already locked

try {
    // Do the record stuff
}        
finally {
    lockedRecords.remove(record);
}

Es verhindert, dass Sie eine Sperre pro Datensatz verwalten müssen, und bietet einen einzigen Ort, falls das Löschen aller Sperren aus irgendeinem Grund erforderlich sein sollte. Wenn Sie jedoch jemals mehr als eine Handvoll Datensätze haben, ist ein echtes HashSet mit Synchronisierung möglicherweise besser geeignet, da die Suchvorgänge zum Hinzufügen / Entfernen O (1) statt linear sind.

Nur eine andere Sichtweise. Kommt nur darauf an, was deine tatsächlichen Threading-Anforderungen sind. Persönlich würde ich ein Collections.synchronizedSet (neues HashSet ()) verwenden, da es sehr schnell sein wird ... die einzige Implikation ist, dass Threads nachgeben können, wenn sie es sonst nicht hätten.

PSpeed
quelle
Wenn Sie die volle Kontrolle haben möchten, können Sie dies in Ihre eigene LockKlasse einordnen, denke ich.
bvdb
1

Eine andere Problemumgehung ist (falls Sie mit den hier gegebenen Antworten keine Chance hatten) die Verwendung von Zeitüberschreitungen. dh unter eins wird nach 1 Sekunde Hängen null zurückgegeben:

ExecutorService executor = Executors.newSingleThreadExecutor();
        //create a callable for the thread
        Future<String> futureTask = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return myObject.getSomething();
            }
        });

        try {
            return futureTask.get(1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            //object is already locked check exception type
            return null;
        }
HRgiger
quelle
-2

Vielen Dank dafür, es hat mir geholfen, eine Rennbedingung zu lösen. Ich habe es ein wenig geändert, um sowohl Gürtel als auch Hosenträger zu tragen.

Hier ist mein Vorschlag für eine Verbesserung der akzeptierten Antwort:

Sie können sicherstellen, dass Sie einen sicheren Zugriff auf die tryLock()Methode erhalten, indem Sie Folgendes tun:

  Lock localLock = new ReentrantLock();

  private void threadSafeCall() {
    boolean isUnlocked = false;

    synchronized(localLock) {
      isUnlocked = localLock.tryLock();
    }

    if (isUnlocked) {
      try {
        rawCall();
      }
      finally {
        localLock.unlock();
      }
    } else {
      LOGGER.log(Level.INFO, "THANKS! - SAVED FROM DOUBLE CALL!");
    }
  }

Dies würde die Situation vermeiden, in der Sie möglicherweise zwei Anrufe erhalten tryLock() fast gleichzeitig , wodurch die Rückgabe möglicherweise zweifelhaft voll ist. Ich würde jetzt gerne, wenn ich falsch liege, hier über Vorsicht sein. Aber hey! Mein Gig ist jetzt stabil :-) ..

Weitere Informationen zu meinen Entwicklungsproblemen finden Sie in meinem Blog .

Der Schwartz
quelle
1
Die Verwendung von synchronisiert für ein Sperrobjekt ist sehr, sehr schlecht. Der Sinn einer Sperre besteht darin, eine Synchronisierung zu vermeiden, sodass Sie eine Zeitüberschreitung oder eine schnelle Rückkehr durchführen können, wenn Sie die Sperre nicht erhalten können.
Ajax