Java Concurrency Utility med JCTools

1. Oversikt

I denne veiledningen introduserer vi biblioteket JCTools (Java Concurrency Tools).

Enkelt sagt, dette gir en rekke verktøy datastrukturer som passer for å jobbe i et flertrådet miljø.

2. Ikke-blokkerende algoritmer

Tradisjonelt bruker flertrådede koder som fungerer på en foranderlig delt tilstand, låser for å sikre datakonsistens og publikasjoner (endringer gjort av en tråd som er synlige for en annen).

Denne tilnærmingen har en rekke ulemper:

  • tråder kan bli blokkert i et forsøk på å skaffe seg en lås, uten å gjøre fremskritt før en annen tråds operasjon er ferdig - dette forhindrer effektivt parallellitet
  • jo tyngre låsekonkurranse er, jo mer tid bruker JVM på å planlegge tråder, håndtere strid og køer med ventetråder og jo mindre ekte arbeid det gjør
  • sperrer er mulig hvis mer enn en lås er involvert og de anskaffes / frigjøres i feil rekkefølge
  • en prioritert inversjonsfare er mulig - en tråd med høy prioritet er låst i et forsøk på å få en lås holdt av en tråd med lav prioritet
  • mesteparten av tiden brukes grovkornede låser, noe som skader parallelliteten mye - finkornet låsing krever mer forsiktig design, øker låsen overhead og er mer feilutsatt

Et alternativ er å bruke a ikke-blokkerende algoritme, dvs. en algoritme der feil eller suspensjon av en hvilken som helst tråd ikke kan forårsake feil eller suspensjon av en annen tråd.

En ikke-blokkerende algoritme er låsfritt hvis minst en av de involverte trådene garantert vil gjøre fremskritt over en vilkårlig tidsperiode, dvs. at det ikke kan oppstå blokkeringer under behandlingen.

Videre er disse algoritmene ventefritt hvis det også er en garantert fremgang per tråd.

Her er en ikke-blokkering Stable eksempel fra den utmerkede Java Concurrency in Practice-boken; den definerer grunntilstanden:

offentlig klasse ConcurrentStack {AtomicReference topp = ny AtomicReference(); privat statisk klasse Node {public E item; offentlig Node neste; // standard konstruktør}}

Og også et par API-metoder:

public void push (E item) {Node newHead = new Node (item); Knutepunkt oldHead; gjør {oldHead = top.get (); newHead.next = oldHead; } while (! top.compareAndSet (oldHead, newHead)); } offentlig E pop () {Node oldHead; Node newHead; gjør {oldHead = top.get (); hvis (oldHead == null) {return null; } newHead = oldHead.next; } while (! top.compareAndSet (oldHead, newHead)); returner oldHead.item; }

Vi kan se at algoritmen bruker finkornet sammenligning og bytte (CAS) instruksjoner og er låsfritt (selv om flere tråder ringer top.compareAndSet () samtidig er en av dem garantert vellykket) men ikke ventefritt siden det ikke er noen garanti for at CAS til slutt lykkes for en bestemt tråd.

3. Avhengighet

La oss først legge til JCTools-avhengigheten til vår pom.xml:

 org.jctools jctools-core 2.1.2 

Vær oppmerksom på at den siste tilgjengelige versjonen er tilgjengelig på Maven Central.

4. JCTools køer

Biblioteket tilbyr en rekke køer som skal brukes i et miljø med flere tråder, dvs. en eller flere tråder skriver til en kø og en eller flere tråder leses fra den på en trådsikker låsfri måte.

Felles grensesnitt for alle implementeringer er org.jctools.queues.MessagePassingQueue.

4.1. Typer køer

Alle køer kan kategoriseres i henhold til produsentens / forbrukerpolitikken:

  • enkelt produsent, enkelt forbruker - slike klasser er navngitt ved hjelp av prefikset Spsc, f.eks. SpscArrayQueue
  • enkelt produsent, flere forbrukere - bruk Spmc prefiks, f.eks. SpmcArrayQueue
  • flere produsenter, én forbruker - bruk Mpsc prefiks, f.eks. MpscArrayQueue
  • flere produsenter, flere forbrukere - bruk Mpmc prefiks, f.eks. MpmcArrayQueue

Det er viktig å merke seg det Det er ingen policy sjekker internt, dvs. en kø kan stille fungere feil ved feil bruk.

F.eks. testen nedenfor fyller a enkeltprodusent kø fra to tråder og passerer selv om forbrukeren ikke garantert ser data fra forskjellige produsenter:

SpscArrayQueue kø = ny SpscArrayQueue (2); Trådprodusent1 = ny tråd (() -> kø.tilbud (1)); produsent1.start (); produsent 1. bli med (); Trådprodusent2 = ny tråd (() -> kø.tilbud (2)); produsent2.start (); produsent 2. bli med (); Sett fraQueue = ny HashSet (); Trådforbruker = ny tråd (() -> kø.drenering (fraQueue :: legg til)); forbruker.start (); forbruker. bli med (); assertThat (fromQueue) .containsOnly (1, 2);

4.2. Køimplementeringer

Oppsummering av klassifiseringene ovenfor, her er listen over JCTools-køer:

  • SpscArrayQueue enkeltprodusent, enkeltforbruker, bruker en matrise internt bundet kapasitet
  • SpscLinkedQueue enkelt produsent, enkelt forbruker, bruker koblet liste internt, ubundet kapasitet
  • SpscChunkedArrayQueue enkeltprodusent, enkeltforbruker, starter med startkapasitet og vokser opp til maksimal kapasitet
  • SpscGrowableArrayQueue enkelt produsent, enkeltforbruker, starter med startkapasitet og vokser opp til maks kapasitet. Dette er den samme kontrakten som SpscChunkedArrayQueue, den eneste forskjellen er intern styring av biter. Det anbefales å bruke SpscChunkedArrayQueue fordi den har en forenklet implementering
  • SpscUnboundedArrayQueue enkeltprodusent, enkeltforbruker, bruker en rekke internt, ubundet kapasitet
  • SpmcArrayQueue enkelt produsent, flere forbrukere, bruker en matrise internt, bundet kapasitet
  • MpscArrayQueue flere produsenter, én forbruker, bruker en rekke internt bundet kapasitet
  • MpscLinkedQueue flere produsenter, én forbruker, bruker en koblet liste internt, ubundet kapasitet
  • MpmcArrayQueue flere produsenter, flere forbrukere, bruker en matrise internt, bundet kapasitet

4.3. Atomic Køer

Alle køer som er nevnt i forrige avsnitt bruker sun.misc. usikre. Men med adventen av Java 9 og JEP-260 blir denne APIen utilgjengelig som standard.

Så det er alternative køer som bruker java.util.concurrent.atomic.AtomicLongFieldUpdater (offentlig API, mindre performant) i stedet for sun.misc. usikre.

De genereres fra køene ovenfor, og navnene deres har ordet Atomisk satt inn mellom, f.eks. SpscChunkedAtomicArrayQueue eller MpmcAtomicArrayQueue.

Det anbefales å bruke ‘vanlige’ køer hvis mulig og ty til AtomicQueues bare i miljøer der sun.misc. usikre er forbudt / ineffektivt som HotSpot Java9 + og JRockit.

4.4. Kapasitet

Alle JCTools-køer kan også ha maksimal kapasitet eller være ubundet. Når en kø er full og den er bundet av kapasitet, slutter den å godta nye elementer.

I det følgende eksemplet:

  • fylle køen
  • sørg for at den slutter å godta nye elementer etter det
  • tøm fra den og sørg for at det er mulig å legge til flere elementer etterpå

Vær oppmerksom på at et par kodeuttalelser slettes for lesbarhet. Den komplette implementeringen finner du på GitHub:

SpscChunkedArrayQueue kø = ny SpscChunkedArrayQueue (8, 16); CountDownLatch startConsuming = ny CountDownLatch (1); CountDownLatch awakeProducer = ny CountDownLatch (1); Trådprodusent = ny tråd (() -> {IntStream.range (0, kø.kapasitet ()). ForEach (i -> {assertThat (kø.offer (i)). IsTrue ();}); assertThat (kø .offer (queue.capacity ())). isFalse (); startConsuming.countDown (); awakeProducer.await (); assertThat (queue.offer (queue.capacity ())). isTrue ();}); produsent.start (); startConsuming.await (); Sett fraQueue = ny HashSet (); queue.drain (fraQueue :: add); awakeProducer.countDown (); produsent. bli med (); queue.drain (fraQueue :: add); assertThat (fromQueue) .containsAll (IntStream.range (0, 17) .boxed (). collect (toSet ()));

5. Andre JCTools datastrukturer

JCTools tilbyr også et par ikke-kø-datastrukturer.

Alle er listet opp nedenfor:

  • NonBlockingHashMap en låsfri ConcurrentHashMap alternativ med bedre skaleringsegenskaper og generelt lavere mutasjonskostnader. Det er implementert via sun.misc. usikre, så det anbefales ikke å bruke denne klassen i et HotSpot Java9 + eller JRockit miljø
  • NonBlockingHashMapLong som NonBlockingHashMap men bruker primitiv lang nøklene
  • NonBlockingHashSet en enkel innpakning NonBlockingHashMapsom JDK java.util.Collections.newSetFromMap ()
  • NonBlockingIdentityHashMap som NonBlockingHashMap men sammenligner nøkler etter identitet.
  • NonBlockingSetIntet flertrådet bitvektorsett implementert som en rekke primitive lengter. Fungerer ineffektivt i tilfelle stille autoboksing

6. Ytelsestesting

La oss bruke JMH for å sammenligne JDK-ene ArrayBlockingQueue mot JCTools kø ytelse. JMH er et open-source mikrobench-rammeverk fra Sun / Oracle JVM-guruer som beskytter oss mot ubestemmelig kompilator / jvm-optimaliseringsalgoritmer). Du er velkommen til å få mer informasjon om det i denne artikkelen.

Merk at kodebiten nedenfor savner et par utsagn for å forbedre lesbarheten. Vennligst finn den komplette kildekoden på GitHub:

offentlig klasse MpmcBenchmark {@Param ({PARAM_UNSAFE, PARAM_AFU, PARAM_JDK}) offentlig ustabil implementering av streng; offentlig flyktig kø; @Benchmark @Group (GROUP_NAME) @GroupThreads (PRODUCER_THREADS_NUMBER) public void write (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement &&! Queue.offer (1L)) {// intentionally left blank}} @Benchmark @ Group (GROUP_NAME) @GroupThreads (CONSUMER_THREADS_NUMBER) public void read (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement && queue.poll () == null) {// intentionally left blank}}}

Resultater (utdrag for 95. persentilen, nanosekunder per operasjon):

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

Vi kan se detMpmcArrayQueue fungerer bare litt bedre enn MpmcAtomicArrayQueue og ArrayBlockingQueue er langsommere med en faktor på to.

7. Ulemper ved bruk av JCTools

Å bruke JCTools har en viktig ulempe - det er ikke mulig å håndheve at bibliotekstimene brukes riktig. Tenk for eksempel på en situasjon når vi begynner å bruke MpscArrayQueue i vårt store og modne prosjekt (merk at det må være en enkelt forbruker).

Dessverre, ettersom prosjektet er stort, er det en mulighet for at noen lager en programmerings- eller konfigurasjonsfeil, og køen blir nå lest fra mer enn en tråd. Systemet ser ut til å fungere som før, men nå er det en sjanse for at forbrukere savner noen meldinger. Det er et reelt problem som kan ha stor innvirkning og det er veldig vanskelig å feilsøke.

Ideelt sett bør det være mulig å kjøre et system med en bestemt systemegenskap som tvinger JCTools til å sikre trådtilgangspolitikk. F.eks. lokale / test / iscenesettelsesmiljøer (men ikke produksjon) kan ha den slått på. Dessverre gir JCTools ikke en slik eiendom.

En annen vurdering er at selv om vi sørget for at JCTools er betydelig raskere enn JDKs motstykke, betyr det ikke at applikasjonen vår får samme hastighet når vi begynner å bruke de tilpassede køimplementeringene. De fleste applikasjoner utveksler ikke mange objekter mellom tråder og er for det meste I / O-bundet.

8. Konklusjon

Vi har nå en grunnleggende forståelse av nytteklassene som tilbys av JCTools og så hvor bra de presterer, sammenlignet med JDKs kolleger under tung belastning.

For å konkludere, Det er verdt å bruke biblioteket bare hvis vi utveksler mange objekter mellom tråder, og til og med da er det nødvendig å være veldig forsiktig med å bevare trådtilgangspolitikken.

Som alltid kan den fullstendige kildekoden for eksemplene ovenfor bli funnet på GitHub.