Met behulp van een Mutex-object in Java

1. Overzicht

In deze tutorial zullen we zien verschillende manieren om een ​​mutex in Java te implementeren.

2. Mutex

In een toepassing met meerdere threads moeten mogelijk twee of meer threads tegelijkertijd toegang hebben tot een gedeelde bron, wat leidt tot onverwacht gedrag. Voorbeelden van dergelijke gedeelde bronnen zijn datastructuren, invoer-uitvoerapparaten, bestanden en netwerkverbindingen.

We noemen dit scenario a race conditie. En het deel van het programma dat toegang heeft tot de gedeelde bron staat bekend als de kritieke sectie. Om een ​​raceconditie te voorkomen, moeten we de toegang tot de kritieke sectie dus synchroniseren.

Een mutex (of wederzijdse uitsluiting) is het eenvoudigste type synchronisator - het zorgt ervoor dat slechts één thread tegelijk het kritieke gedeelte van een computerprogramma kan uitvoeren.

Om toegang te krijgen tot een kritieke sectie, verkrijgt een thread de mutex, opent vervolgens de kritieke sectie en geeft uiteindelijk de mutex vrij. In de tussentijd, alle andere threads blokkeren totdat de mutex wordt vrijgegeven. Zodra een thread de kritieke sectie verlaat, kan een andere thread de kritieke sectie binnengaan.

3. Waarom Mutex?

Laten we eerst een voorbeeld nemen van een SequenceGeneraror class, die de volgende reeks genereert door de huidige waarde telkens met één:

openbare klasse SequenceGenerator {privé int currentValue = 0; openbare int getNextSequence () {currentValue = currentValue + 1; return currentValue; }}

Laten we nu een testcase maken om te zien hoe deze methode zich gedraagt ​​wanneer meerdere threads er tegelijkertijd toegang toe proberen te krijgen:

@Test openbare leegte gegevenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior () gooit Uitzondering {int count = 1000; Set uniqueSequences = getUniqueSequences (new SequenceGenerator (), count); Assert.assertEquals (count, uniqueSequences.size ()); } private Set getUniqueSequences (SequenceGenerator generator, int count) gooit uitzondering {ExecutorService executor = Executors.newFixedThreadPool (3); Set uniqueSequences = new LinkedHashSet (); Lijst futures = nieuwe ArrayList (); for (int i = 0; i <count; i ++) {futures.add (executor.submit (generator :: getNextSequence)); } voor (Future future: futures) {uniqueSequences.add (future.get ()); } executor.awaitTermination (1, TimeUnit.SECONDS); executor.shutdown (); unieke sequenties teruggeven; }

Zodra we deze testcase hebben uitgevoerd, kunnen we zien dat deze meestal mislukt met de reden vergelijkbaar met:

java.lang.AssertionError: verwacht: maar was: op org.junit.Assert.fail (Assert.java:88) op org.junit.Assert.failNotEquals (Assert.java:834) op org.junit.Assert.assertEquals ( Assert.java:645)

De uniqueSequences zou de grootte moeten hebben die gelijk is aan het aantal keren dat we de getNextSequence methode in onze testcase. Dit is echter niet het geval vanwege de raceconditie. Het is duidelijk dat we dit gedrag niet willen.

Dus om dergelijke raceomstandigheden te vermijden, moeten we dat doen zorg ervoor dat slechts één thread de getNextSequence methode per keer. In dergelijke scenario's kunnen we een mutex gebruiken om de threads te synchroniseren.

Er zijn verschillende manieren waarop we een mutex in Java kunnen implementeren. Dus nu zullen we de verschillende manieren zien om een ​​mutex voor ons te implementeren SequenceGenerator klasse.

4. Met behulp van gesynchroniseerd Trefwoord

Eerst bespreken we de gesynchroniseerd trefwoord, wat de eenvoudigste manier is om een ​​mutex in Java te implementeren.

Elk object in Java heeft een intrinsiek slot dat eraan is gekoppeld. Degesynchroniseerd methode ende gesynchroniseerd blok gebruik deze intrinsieke vergrendeling om de toegang van de kritieke sectie te beperken tot slechts één thread tegelijk.

Daarom, wanneer een thread een gesynchroniseerd methode of voert een gesynchroniseerd blok, het verkrijgt automatisch het slot. Het slot wordt vrijgegeven wanneer de methode of het blok is voltooid of er een uitzondering van wordt gemaakt.

Laten we veranderen getNextSequence om een ​​mutex te hebben, simpelweg door de gesynchroniseerd trefwoord:

openbare klasse SequenceGeneratorUsingSynchronizedMethod breidt SequenceGenerator uit {@Override openbaar gesynchroniseerd int getNextSequence () {retour super.getNextSequence (); }}

De gesynchroniseerd block is vergelijkbaar met het gesynchroniseerd methode, met meer controle over de kritieke sectie en het object dat we kunnen gebruiken voor vergrendeling.

Dus laten we nu kijken hoe we dat kunnen gebruik de gesynchroniseerd blok om te synchroniseren op een aangepast mutex-object:

openbare klasse SequenceGeneratorUsingSynchronizedBlock breidt SequenceGenerator {privé-object mutex = nieuw Object (); @Override public int getNextSequence () {gesynchroniseerd (mutex) {retourneer super.getNextSequence (); }}}

5. Met behulp van ReentrantLock

De ReentrantLock class werd geïntroduceerd in Java 1.5. Het biedt meer flexibiliteit en controle dan de gesynchroniseerd trefwoord benadering.

Laten we eens kijken hoe we de ReentrantLock om wederzijdse uitsluiting te bereiken:

openbare klasse SequenceGeneratorUsingReentrantLock breidt SequenceGenerator uit {privé ReentrantLock mutex = nieuwe ReentrantLock (); @Override public int getNextSequence () {probeer {mutex.lock (); retourneer super.getNextSequence (); } eindelijk {mutex.unlock (); }}}

6. Met behulp van Semafoor

Leuk vinden ReentrantLock, de Semafoor class werd ook geïntroduceerd in Java 1.5.

Terwijl in het geval van een mutex slechts één thread toegang heeft tot een kritieke sectie, Semafoor staat toe een vast aantal threads om toegang te krijgen tot een kritieke sectie. Daarom we kunnen ook een mutex implementeren door het aantal toegestane threads in een Semafoor tot een.

Laten we nu een nieuwe threadveilige versie van SequenceGenerator gebruik makend van Semafoor:

openbare klasse SequenceGeneratorUsingSemaphore breidt SequenceGenerator uit {private Semaphore mutex = nieuwe Semaphore (1); @Override public int getNextSequence () {probeer {mutex.acquire (); retourneer super.getNextSequence (); } catch (InterruptedException e) {// code voor het afhandelen van uitzonderingen} tenslotte {mutex.release (); }}}

7. Guava's gebruiken Monitor Klasse

Tot nu toe hebben we de opties gezien om mutex te implementeren met behulp van functies die door Java worden geboden.

echter, de Monitor klasse van de Guava-bibliotheek van Google is een beter alternatief voor de ReentrantLock klasse. Zoals aangegeven in de documentatie, codeert u met Monitor is beter leesbaar en minder foutgevoelig dan de code die gebruikt ReentrantLock.

Eerst zullen we de Maven-afhankelijkheid voor Guava toevoegen:

 com.google.guava guave 28.0-jre 

Nu zullen we nog een subklasse schrijven van SequenceGenerator de ... gebruiken Monitor klasse:

openbare klasse SequenceGeneratorUsingMonitor breidt SequenceGenerator {private Monitor mutex = nieuwe Monitor (); @Override public int getNextSequence () {mutex.enter (); probeer {return super.getNextSequence (); } eindelijk {mutex.leave (); }}}

8. Conclusie

In deze tutorial hebben we het concept van een mutex onderzocht. We hebben ook de verschillende manieren gezien om het in Java te implementeren.

Zoals altijd is de volledige broncode van de codevoorbeelden die in deze tutorial worden gebruikt, beschikbaar op GitHub.