Java-hulpprogramma voor gelijktijdigheid met JCTools

1. Overzicht

In deze tutorial introduceren we de JCTools-bibliotheek (Java Concurrency Tools).

Simpel gezegd, dit levert een aantal datastructuren op die geschikt zijn voor het werken in een multi-threaded omgeving.

2. Niet-blokkerende algoritmen

Traditioneel gebruikt multi-threaded code die op een veranderlijke gedeelde status werkt, vergrendelingen om gegevensconsistentie en publicaties te garanderen (wijzigingen aangebracht door de ene thread die zichtbaar zijn voor de andere).

Deze aanpak heeft een aantal nadelen:

  • threads kunnen geblokkeerd raken in een poging om een ​​lock te krijgen en geen vooruitgang boeken totdat de bewerking van een andere thread is voltooid - dit voorkomt effectief parallellisme
  • hoe zwaarder de lock-stelling is, hoe meer tijd de JVM besteedt aan het verwerken van threads, het beheren van conflicten en wachtrijen van wachtende threads en hoe minder echt werk het doet
  • deadlocks zijn mogelijk als er meer dan één sluis bij betrokken is en ze in de verkeerde volgorde worden verworven / vrijgegeven
  • gevaar voor omkering met prioriteit is mogelijk - een thread met hoge prioriteit wordt vergrendeld in een poging om een ​​vergrendeling te krijgen die wordt vastgehouden door een thread met lage prioriteit
  • meestal worden grofkorrelige sloten gebruikt, wat parallellisme veel pijn doet - fijnkorrelige vergrendeling vereist een zorgvuldiger ontwerp, verhoogt de vergrendeling boven het hoofd en is meer vatbaar voor fouten

Een alternatief is om een niet-blokkerend algoritme, d.w.z. een algoritme waarbij het falen of opschorten van een thread niet kan leiden tot het falen of opschorten van een andere thread.

Een niet-blokkerend algoritme is slotvrij als ten minste een van de betrokken threads gegarandeerd vooruitgang boekt over een willekeurige tijdsperiode, d.w.z. dat er geen impasses kunnen ontstaan ​​tijdens de verwerking.

Bovendien zijn deze algoritmen wachtvrij als er ook een gegarandeerde voortgang per thread is.

Hier is een niet-blokkerende Stapel voorbeeld uit het uitstekende Java Concurrency in Practice-boek; het definieert de basistoestand:

openbare klasse ConcurrentStack {AtomicReference top = nieuwe AtomicReference(); privé statische klasse Node {openbaar E-item; openbaar knooppunt volgende; // standaard constructor}}

En ook een paar API-methoden:

public void push (E-item) {Node newHead = new Node (item); Knooppunt oldHead; doe {oldHead = top.get (); newHead.next = oldHead; } while (! top.compareAndSet (oldHead, newHead)); } public E pop () {Node oldHead; Knooppunt newHead; doe {oldHead = top.get (); if (oldHead == null) {retourneer null; } newHead = oldHead.next; } while (! top.compareAndSet (oldHead, newHead)); retourneer oldHead.item; }

We kunnen zien dat het algoritme fijnmazige vergelijk-en-ruilinstructies (CAS) gebruikt en dat ook is slotvrij (zelfs als meerdere threads top.compareAndSet () tegelijkertijd is een van hen gegarandeerd succesvol) maar niet wachtvrij aangezien er geen garantie is dat CAS uiteindelijk zal slagen voor een bepaalde thread.

3. Afhankelijkheid

Laten we eerst de JCTools-afhankelijkheid toevoegen aan onze pom.xml:

 org.jctools jctools-core 2.1.2 

Houd er rekening mee dat de nieuwste beschikbare versie beschikbaar is op Maven Central.

4. JCTools-wachtrijen

De bibliotheek biedt een aantal wachtrijen om te gebruiken in een multi-threaded omgeving, d.w.z. een of meer threads schrijven naar een wachtrij en een of meer threads die eruit worden gelezen op een threadveilige, vergrendelingsvrije manier.

De gemeenschappelijke interface voor iedereen Wachtrij implementaties is org.jctools.queues.MessagePassingQueue.

4.1. Soorten wachtrijen

Alle wachtrijen kunnen worden gecategoriseerd op basis van hun producenten- / consumentenbeleid:

  • één producent, één consument - dergelijke klassen worden genoemd met het voorvoegsel Spsc, b.v. SpscArrayQueue
  • één producent, meerdere consumenten - gebruik Spmc voorvoegsel, bijv. SpmcArrayQueue
  • meerdere producenten, één consument - gebruik Mpsc voorvoegsel, bijv. MpscArrayQueue
  • meerdere producenten, meerdere consumenten - gebruik Mpmc voorvoegsel, bijv. MpmcArrayQueue

Het is belangrijk om dat op te merken er zijn geen interne beleidscontroles, d.w.z. een wachtrij kan stilzwijgend defect raken in geval van onjuist gebruik.

Bijv. de onderstaande test vult een single-producer wachtrij van twee threads en passages, hoewel de consument niet gegarandeerd gegevens van verschillende producenten te zien krijgt:

SpscArrayQueue wachtrij = nieuwe SpscArrayQueue (2); Thread producer1 = nieuwe Thread (() -> queue.offer (1)); producer1.start (); producer1.join (); Thread producer2 = nieuwe Thread (() -> queue.offer (2)); producer2.start (); producer2.join (); Set fromQueue = nieuwe HashSet (); Thread consument = nieuwe Thread (() -> queue.drain (fromQueue :: add)); consument.start (); consument.join (); assertThat (fromQueue) .containsOnly (1, 2);

4.2. Wachtrij-implementaties

Om de bovenstaande classificaties samen te vatten, is hier de lijst met JCTools-wachtrijen:

  • SpscArrayQueue één producent, één consument, gebruikt intern een array, gebonden capaciteit
  • SpscLinkedQueue één producent, één consument, gebruikt intern gekoppelde lijst, ongebonden capaciteit
  • SpscChunkedArrayQueue enkele producent, enkele consument, begint met initiële capaciteit en groeit op tot maximale capaciteit
  • SpscGrowableArrayQueue enkele producent, enkele consument, begint met initiële capaciteit en groeit op tot maximale capaciteit. Dit is hetzelfde contract als SpscChunkedArrayQueue, het enige verschil is het interne brokkenbeheer. Het wordt aanbevolen om te gebruiken SpscChunkedArrayQueue omdat het een vereenvoudigde implementatie heeft
  • SpscUnboundedArrayQueue één producent, één consument, gebruikt een interne array, ongebonden capaciteit
  • SpmcArrayQueue één producent, meerdere consumenten, gebruikt een interne array, gebonden capaciteit
  • MpscArrayQueue meerdere producenten, één consument, gebruiken intern een array, gebonden capaciteit
  • MpscLinkedQueue meerdere producenten, één consument, gebruikt intern een gekoppelde lijst, ongebonden capaciteit
  • MpmcArrayQueue meerdere producenten, meerdere consumenten, gebruiken intern een array, gebonden capaciteit

4.3. Atomic wachtrijen

Alle wachtrijen die in de vorige sectie worden genoemd, gebruiken zon.misc. onveilig. Met de komst van Java 9 en de JEP-260 wordt deze API echter standaard ontoegankelijk.

Er zijn dus alternatieve wachtrijen die java.util.concurrent.atomic.AtomicLongFieldUpdater (openbare API, minder performant) in plaats van zon.misc. onveilig.

Ze worden gegenereerd op basis van de bovenstaande wachtrijen en hun namen hebben het woord Atomic daartussen ingevoegd, b.v. SpscChunkedAtomicArrayQueue of MpmcAtomicArrayQueue.

Het wordt aanbevolen om indien mogelijk ‘gewone’ wachtrijen te gebruiken AtomicQueues alleen in omgevingen waar zon.misc. onveilig is verboden / ineffectief zoals HotSpot Java9 + en JRockit.

4.4. Capaciteit

Alle JCTools-wachtrijen hebben mogelijk ook een maximale capaciteit of zijn ongebonden. Wanneer een wachtrij vol is en aan capaciteit gebonden is, accepteert deze geen nieuwe elementen meer.

In het volgende voorbeeld:

  • vul de wachtrij
  • zorg ervoor dat het daarna stopt met het accepteren van nieuwe elementen
  • afvoer eruit en zorg ervoor dat het mogelijk is om daarna meer elementen toe te voegen

Houd er rekening mee dat een aantal codeverklaringen voor leesbaarheid zijn verwijderd. De volledige implementatie is te vinden op GitHub:

SpscChunkedArrayQueue wachtrij = nieuwe SpscChunkedArrayQueue (8, 16); CountDownLatch startConsuming = nieuwe CountDownLatch (1); CountDownLatch awakeProducer = nieuwe CountDownLatch (1); Thread producer = nieuwe Thread (() -> {IntStream.range (0, queue.capacity ()). ForEach (i -> {assertThat (queue.offer (i)). IsTrue ();}); assertThat (wachtrij .offer (queue.capacity ())). isFalse (); startConsuming.countDown (); awakeProducer.await (); assertThat (queue.offer (queue.capacity ())). isTrue ();}); producer.start (); startConsuming.await (); Set fromQueue = nieuwe HashSet (); queue.drain (fromQueue :: add); awakeProducer.countDown (); producer.join (); queue.drain (fromQueue :: add); assertThat (fromQueue) .containsAll (IntStream.range (0, 17) .boxed (). collect (toSet ()));

5. Andere JCTools-gegevensstructuren

JCTools biedt ook een aantal niet-wachtrij datastructuren.

Ze zijn allemaal hieronder opgesomd:

  • NonBlockingHashMap een slotvrij ConcurrentHashMap alternatief met betere schaaleigenschappen en doorgaans lagere mutatiekosten. Het wordt geïmplementeerd via zon.misc. onveilig, dus het wordt niet aanbevolen om deze klasse te gebruiken in een HotSpot Java9 + of JRockit-omgeving
  • NonBlockingHashMapLong Leuk vinden NonBlockingHashMap maar gebruikt primitief lang sleutels
  • NonBlockingHashSet een simpele wrapper eromheen NonBlockingHashMapzoals JDK's java.util.Collections.newSetFromMap ()
  • NonBlockingIdentityHashMap Leuk vinden NonBlockingHashMap maar vergelijkt sleutels op identiteit.
  • NonBlockingSetInteen multi-threaded bit-vector set geïmplementeerd als een array van primitieve verlangt. Werkt niet effectief in het geval van stille autoboxing

6. Prestatietests

Laten we JMH gebruiken om de JDK's te vergelijken ArrayBlockingQueue versus de prestaties van de JCTools-wachtrij. JMH is een open-source micro-benchmarkraamwerk van Sun / Oracle JVM-goeroes dat ons beschermt tegen ondeterminisme van compiler / jvm-optimalisatiealgoritmen). Aarzel niet om er in dit artikel meer informatie over te krijgen.

Merk op dat het onderstaande codefragment een aantal uitspraken mist om de leesbaarheid te verbeteren. Vind de volledige broncode op GitHub:

openbare klasse MpmcBenchmark {@Param ({PARAM_UNSAFE, PARAM_AFU, PARAM_JDK}) openbare vluchtige String-implementatie; openbare vluchtige wachtrij; @Benchmark @Group (GROUP_NAME) @GroupThreads (PRODUCER_THREADS_NUMBER) public void write (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement &&! Queue.offer (1L)) {// opzettelijk blanco gelaten}} @Benchmark @ Group (GROUP_NAME) @GroupThreads (CONSUMER_THREADS_NUMBER) public void read (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement && queue.poll () == null) {// opzettelijk blanco gelaten}}}

Resultaten (fragment voor het 95e percentiel, nanoseconden per bewerking):

MpmcBenchmark.MyGroup: MyGroup · p0.95 MpmcArrayQueue sample 1052.000 ns / op MpmcBenchmark.MyGroup: MyGroup · p0.95 MpmcAtomicArrayQueue sample 1106.000 ns / op MpmcBenchmark.MyGroup: MyGroup · p0.95 ns / op MpmcBenchmark.MyGroup: MyGroup · p0.95 ns / op MpmcBenchmark.MyGroup: MyGroup · p0.95 ns / op

Dat kunnen we zienMpmcArrayQueue presteert net iets beter dan MpmcAtomicArrayQueue en ArrayBlockingQueue is een factor twee langzamer.

7. Nadelen van het gebruik van JCTools

Het gebruik van JCTools heeft een belangrijk nadeel: het is niet mogelijk om af te dwingen dat de bibliotheekklassen correct worden gebruikt. Denk bijvoorbeeld aan een situatie wanneer we beginnen met gebruiken MpscArrayQueue in ons grote en volwassen project (merk op dat er één consument moet zijn).

Helaas, aangezien het project groot is, bestaat de mogelijkheid dat iemand een programmeer- of configuratiefout maakt en dat de wachtrij nu uit meer dan één thread wordt gelezen. Het systeem lijkt te werken zoals voorheen, maar nu bestaat de kans dat consumenten berichten missen. Dat is een reëel probleem dat een grote impact kan hebben en erg moeilijk te debuggen is.

Idealiter zou het mogelijk moeten zijn om een ​​systeem uit te voeren met een bepaalde systeemeigenschap die JCTools dwingt om een ​​threadtoegangsbeleid te garanderen. Bijv. lokale / test / staging-omgevingen (maar niet in productie) hebben het mogelijk ingeschakeld. Helaas biedt JCTools een dergelijke eigenschap niet.

Een andere overweging is dat, hoewel we ervoor hebben gezorgd dat JCTools aanzienlijk sneller is dan de tegenhanger van JDK, dit niet betekent dat onze applicatie evenveel snelheid wint als we de aangepaste wachtrij-implementaties gaan gebruiken. De meeste applicaties wisselen niet veel objecten uit tussen threads en zijn meestal I / O-gebonden.

8. Conclusie

We hebben nu een basiskennis van de hulpprogramma-klassen die door JCTools worden aangeboden en zagen hoe goed ze presteren in vergelijking met de tegenhangers van de JDK onder zware belasting.

Tot slot, het is de moeite waard om de bibliotheek alleen te gebruiken als we veel objecten tussen threads uitwisselen en zelfs dan is het noodzakelijk om heel voorzichtig te zijn om het threadtoegangsbeleid te behouden.

Zoals altijd is de volledige broncode voor de bovenstaande voorbeelden te vinden op GitHub.