Verwenden eines Mutex-Objekts in Java

1. Übersicht

In diesem Tutorial sehen wir verschiedene Möglichkeiten, einen Mutex in Java zu implementieren .

2. Mutex

In einer Multithread-Anwendung müssen möglicherweise zwei oder mehr Threads gleichzeitig auf eine gemeinsam genutzte Ressource zugreifen, was zu unerwartetem Verhalten führt. Beispiele für solche gemeinsam genutzten Ressourcen sind Datenstrukturen, Eingabe- / Ausgabegeräte, Dateien und Netzwerkverbindungen.

Wir nennen dieses Szenario eine Rennbedingung . Der Teil des Programms, der auf die gemeinsam genutzte Ressource zugreift, wird als kritischer Abschnitt bezeichnet . Um eine Rennbedingung zu vermeiden, müssen wir den Zugriff auf den kritischen Abschnitt synchronisieren.

Ein Mutex (oder gegenseitiger Ausschluss) ist die einfachste Art von Synchronisierer - er stellt sicher, dass jeweils nur ein Thread den kritischen Abschnitt eines Computerprogramms ausführen kann .

Um auf einen kritischen Abschnitt zuzugreifen, erfasst ein Thread den Mutex, greift dann auf den kritischen Abschnitt zu und gibt schließlich den Mutex frei. In der Zwischenzeit blockieren alle anderen Threads, bis der Mutex freigegeben wird. Sobald ein Thread den kritischen Abschnitt verlässt, kann ein anderer Thread in den kritischen Abschnitt eintreten.

3. Warum Mutex?

Nehmen wir zunächst ein Beispiel für eine SequenceGeneraror- Klasse, die die nächste Sequenz generiert, indem sie den aktuellen Wert jedes Mal um eins erhöht :

public class SequenceGenerator { private int currentValue = 0; public int getNextSequence() { currentValue = currentValue + 1; return currentValue; } }

Erstellen wir nun einen Testfall, um zu sehen, wie sich diese Methode verhält, wenn mehrere Threads gleichzeitig versuchen, darauf zuzugreifen:

@Test public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception { int count = 1000; Set uniqueSequences = getUniqueSequences(new SequenceGenerator(), count); Assert.assertEquals(count, uniqueSequences.size()); } private Set getUniqueSequences(SequenceGenerator generator, int count) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(3); Set uniqueSequences = new LinkedHashSet(); List
    
      futures = new ArrayList(); for (int i = 0; i < count; i++) { futures.add(executor.submit(generator::getNextSequence)); } for (Future future : futures) { uniqueSequences.add(future.get()); } executor.awaitTermination(1, TimeUnit.SECONDS); executor.shutdown(); return uniqueSequences; }
    

Sobald wir diesen Testfall ausgeführt haben, können wir feststellen, dass er die meiste Zeit aus folgenden Gründen fehlschlägt:

java.lang.AssertionError: expected: but was: at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:645)

Die Größe von uniqueSequences soll der Größe entsprechen, mit der wir die Methode getNextSequence in unserem Testfall ausgeführt haben. Dies ist jedoch aufgrund der Rennbedingungen nicht der Fall. Offensichtlich wollen wir dieses Verhalten nicht.

Um solche Race-Bedingungen zu vermeiden, müssen wir sicherstellen, dass jeweils nur ein Thread die Methode getNextSequence ausführen kann . In solchen Szenarien können wir einen Mutex verwenden, um die Threads zu synchronisieren.

Es gibt verschiedene Möglichkeiten, einen Mutex in Java zu implementieren. Als nächstes sehen wir uns die verschiedenen Möglichkeiten an, einen Mutex für unsere SequenceGenerator- Klasse zu implementieren .

4. Verwenden des synchronisierten Schlüsselworts

Zuerst werden wir das synchronisierte Schlüsselwort diskutieren , das der einfachste Weg ist, einen Mutex in Java zu implementieren.

Jedem Objekt in Java ist eine intrinsische Sperre zugeordnet. Die synchronisierte Methode und der synchronisierte Block verwenden diese intrinsische Sperre , um den Zugriff auf den kritischen Abschnitt auf jeweils nur einen Thread zu beschränken.

Wenn ein Thread eine synchronisierte Methode aufruft oder in einen synchronisierten Block eintritt , erhält er daher automatisch die Sperre. Die Sperre wird aufgehoben, wenn die Methode oder der Block abgeschlossen ist oder eine Ausnahme von ihnen ausgelöst wird.

Ändern Sie getNextSequence in einen Mutex, indem Sie einfach das synchronisierte Schlüsselwort hinzufügen :

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator { @Override public synchronized int getNextSequence() { return super.getNextSequence(); } }

Der synchronisierte Block ähnelt der synchronisierten Methode, mit mehr Kontrolle über den kritischen Abschnitt und das Objekt, das wir zum Sperren verwenden können.

Lassen Sie uns nun sehen, wie wir den synchronisierten Block verwenden können, um ein benutzerdefiniertes Mutex-Objekt zu synchronisieren :

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator { private Object mutex = new Object(); @Override public int getNextSequence() { synchronized (mutex) { return super.getNextSequence(); } } }

5. Verwenden von ReentrantLock

Die ReentrantLock- Klasse wurde in Java 1.5 eingeführt. Es bietet mehr Flexibilität und Kontrolle als der synchronisierte Keyword-Ansatz.

Mal sehen, wie wir das ReentrantLock verwenden können , um einen gegenseitigen Ausschluss zu erreichen:

public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator { private ReentrantLock mutex = new ReentrantLock(); @Override public int getNextSequence() { try { mutex.lock(); return super.getNextSequence(); } finally { mutex.unlock(); } } }

6. Verwenden von Semaphore

Wie ReentrantLock wurde auch die Semaphore- Klasse in Java 1.5 eingeführt.

Während im Fall eines Mutex nur ein Thread auf einen kritischen Abschnitt zugreifen kann, ermöglicht Semaphore einer festen Anzahl von Threads den Zugriff auf einen kritischen Abschnitt . Daher können wir auch einen Mutex implementieren, indem wir die Anzahl der zulässigen Threads in einem Semaphor auf eins setzen .

Lassen Sie uns nun eine weitere thread-sichere Version von SequenceGenerator mit Semaphore erstellen :

public class SequenceGeneratorUsingSemaphore extends SequenceGenerator { private Semaphore mutex = new Semaphore(1); @Override public int getNextSequence() { try { mutex.acquire(); return super.getNextSequence(); } catch (InterruptedException e) { // exception handling code } finally { mutex.release(); } } }

7. Verwenden der Monitor- Klasse von Guava

Bisher haben wir die Optionen zur Implementierung von Mutex mithilfe der von Java bereitgestellten Funktionen gesehen.

Die Monitor- Klasse der Guava-Bibliothek von Google ist jedoch eine bessere Alternative zur ReentrantLock- Klasse. Gemäß der Dokumentation ist Code mit Monitor besser lesbar und weniger fehleranfällig als der Code mit ReentrantLock .

Zuerst fügen wir die Maven-Abhängigkeit für Guava hinzu:

 com.google.guava guava 28.0-jre 

Jetzt schreiben wir eine weitere Unterklasse von SequenceGenerator mit der Monitor- Klasse:

public class SequenceGeneratorUsingMonitor extends SequenceGenerator { private Monitor mutex = new Monitor(); @Override public int getNextSequence() { mutex.enter(); try { return super.getNextSequence(); } finally { mutex.leave(); } } }

8. Fazit

In diesem Tutorial haben wir uns mit dem Konzept eines Mutex befasst. Wir haben auch die verschiedenen Möglichkeiten gesehen, es in Java zu implementieren.

Wie immer ist der vollständige Quellcode der in diesem Tutorial verwendeten Codebeispiele auf GitHub verfügbar.