Java Concurrency Utility med JCTools

1. Oversigt

I denne vejledning introducerer vi biblioteket JCTools (Java Concurrency Tools).

Kort sagt, dette giver en række hjælpedatastrukturer, der er egnede til at arbejde i et miljø med flere tråde.

2. Ikke-blokerende algoritmer

Traditionelt bruger kode med flere tråde, der fungerer i en ændret delt tilstand, låse for at sikre datakonsistens og publikationer (ændringer foretaget af en tråd, der er synlige for en anden).

Denne tilgang har en række ulemper:

  • tråde kan blive blokeret i et forsøg på at erhverve en lås, hvilket ikke gør fremskridt, før en anden tråds operation er afsluttet - dette forhindrer effektivt parallelisme
  • jo tungere låsekonflikt er, jo mere tid bruger JVM på at planlægge tråde, styre strid og køer på ventetråde og jo mindre ægte arbejde det udfører
  • blokeringer er mulige, hvis mere end en lås er involveret, og de er erhvervet / frigivet i forkert rækkefølge
  • en prioritetsinversionsfare er mulig - en tråd med høj prioritet er låst i et forsøg på at få en lås, der holdes af en tråd med lav prioritet
  • det meste af tiden anvendes grovkornede låse, hvilket skader parallelisme meget - finkornet låsning kræver mere omhyggeligt design, øger låsen over hovedet og er mere udsat for fejl

Et alternativ er at bruge en ikke-blokerende algoritme, dvs. en algoritme, hvor fejl eller suspension af en tråd ikke kan forårsage fejl eller suspension af en anden tråd.

En ikke-blokerende algoritme er låsefri hvis mindst en af ​​de involverede tråde garanteres at gøre fremskridt over en vilkårlig periode, dvs. der kan ikke opstå blokeringer under behandlingen.

Desuden er disse algoritmer ventefri hvis der også er en garanteret fremgang pr. tråd.

Her er en ikke-blokering Stak eksempel fra den fremragende Java Concurrency in Practice-bog; den definerer den grundlæggende tilstand:

offentlig klasse ConcurrentStack {AtomicReference top = ny AtomicReference(); privat statisk klasse Node {offentlig E-vare; offentlig knude næste; // standardkonstruktør}}

Og også et par API-metoder:

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

Vi kan se, at algoritmen bruger finkornede sammenlignings-og-swap-instruktioner (CAS) og er låsefri (selvom flere tråde kalder top.compareAndSet () samtidig er en af ​​dem garanteret en succes), men ikke ventefri da der ikke er nogen garanti for, at CAS i sidste ende lykkes for en bestemt tråd.

3. Afhængighed

Lad os først tilføje JCTools afhængighed til vores pom.xml:

 org.jctools jctools-core 2.1.2 

Bemærk, at den senest tilgængelige version er tilgængelig på Maven Central.

4. JCTools køer

Biblioteket tilbyder et antal køer, der skal bruges i et miljø med flere tråde, dvs. en eller flere tråde skriver til en kø, og en eller flere tråde læses fra den på en trådsikker låsfri måde.

Den fælles grænseflade for alle implementeringer er org.jctools.queues.MessagePassingQueue.

4.1. Typer af køer

Alle køer kan kategoriseres efter deres producent / forbrugerpolitik:

  • enkelt producent, enkeltforbruger - sådanne klasser navngives ved hjælp af præfikset Spsc, f.eks. SpscArrayQueue
  • enkelt producent, flere forbrugere - brug Spmc præfiks, f.eks. SpmcArrayQueue
  • flere producenter, en enkelt forbruger - brug Mpsc præfiks, f.eks. MpscArrayQueue
  • flere producenter, flere forbrugere - brug Mpmc præfiks, f.eks. MpmcArrayQueue

Det er vigtigt at bemærke det der er ingen politikchecks internt, dvs. en kø kan stille og roligt fungere forkert i tilfælde af forkert brug.

F.eks. nedenstående test udfylder a enkeltproducent kø fra to tråde og passerer, selvom forbrugeren ikke garanteres at se data fra forskellige producenter:

SpscArrayQueue kø = ny SpscArrayQueue (2); Trådproducent1 = ny tråd (() -> kø. Tilbud (1)); producent1.start (); producent 1. slutte sig til (); Trådproducent2 = ny tråd (() -> kø. Tilbud (2)); producent2.start (); producer2.join (); Indstil fraQueue = ny HashSet (); Trådforbruger = ny tråd (() -> kø.dræn (fraQueue :: tilføj)); forbruger.start (); forbruger.tilslut (); hævder, at (fraQueue). indeholder kun (1, 2);

4.2. Køimplementeringer

Sammenfattende klassificeringerne ovenfor er her listen over JCTools-køer:

  • SpscArrayQueue enkelt producent, enkeltforbruger, bruger en matrix internt bundet kapacitet
  • SpscLinkedQueue enkelt producent, enkeltforbruger, bruger internt tilknyttet liste, ubundet kapacitet
  • SpscChunkedArrayQueue enkelt producent, enkeltforbruger, starter med startkapacitet og vokser op til maksimal kapacitet
  • SpscGrowableArrayQueue enkelt producent, enkeltforbruger, starter med startkapacitet og vokser op til maksimal kapacitet. Dette er den samme kontrakt som SpscChunkedArrayQueue, den eneste forskel er intern styring af klumper. Det anbefales at bruge SpscChunkedArrayQueue fordi det har en forenklet implementering
  • SpscUnboundedArrayQueue enkelt producent, enkeltforbruger, bruger en matrix internt, ubundet kapacitet
  • SpmcArrayQueue enkelt producent, flere forbrugere, bruger en matrix internt, bundet kapacitet
  • MpscArrayQueue flere producenter, en enkelt forbruger, bruger en matrix internt, bundet kapacitet
  • MpscLinkedQueue flere producenter, én forbruger, bruger en sammenkædet liste internt, ubunden kapacitet
  • MpmcArrayQueue flere producenter, flere forbrugere, bruger en matrix internt, bundet kapacitet

4.3. Atomiske køer

Alle køer nævnt i det forrige afsnit bruger sun.misc. usikker. Men med fremkomsten af ​​Java 9 og JEP-260 bliver denne API som standard utilgængelig.

Så der er alternative køer, der bruger java.util.concurrent.atomic.AtomicLongFieldUpdater (offentlig API, mindre performant) i stedet for sun.misc. usikker.

De genereres fra køerne ovenfor, og deres navne har ordet Atomar indsat imellem, f.eks. SpscChunkedAtomicArrayQueue eller MpmcAtomicArrayQueue.

Det anbefales at bruge 'almindelige' køer, hvis det er muligt, og ty til AtomicQueues kun i miljøer hvor sun.misc. usikker er forbudt / ineffektiv som HotSpot Java9 + og JRockit.

4.4. Kapacitet

Alle JCTools-køer kan muligvis også have en maksimal kapacitet eller være ubundet. Når en kø er fuld, og den er bundet af kapacitet, stopper den med at acceptere nye elementer.

I det følgende eksempel:

  • udfyld køen
  • sørg for, at den holder op med at acceptere nye elementer efter det
  • dræne fra det og sikre, at det er muligt at tilføje flere elementer bagefter

Vær opmærksom på, at et par kodepunkter udsættes for læsbarhed. Den komplette implementering kan findes på GitHub:

SpscChunkedArrayQueue kø = ny SpscChunkedArrayQueue (8, 16); CountDownLatch startConsuming = ny CountDownLatch (1); CountDownLatch awakeProducer = ny CountDownLatch (1); Trådproducent = ny tråd (() -> {IntStream.range (0, kø.kapacitet ()). ForEach (i -> {assertThat (kø.tilbud (i)). IsTrue ();}); assertThat (kø .offer (queue.capacity ())). isFalse (); startConsuming.countDown (); awakeProducer.await (); assertThat (queueoffer (queue.capacity ())). isTrue ();}); producer.start (); startConsuming.await (); Indstil fraQueue = ny HashSet (); kø.drain (fraQueue :: tilføj); awakeProducer.countDown (); producer.join (); kø.drain (fraQueue :: tilføj); assertThat (fromQueue) .containsAll (IntStream.range (0, 17) .boxed (). collect (toSet ()));

5. Andre JCTools datastrukturer

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

Alle er anført nedenfor:

  • NonBlockingHashMap en låsefri ConcurrentHashMap alternativ med bedre skaleringsegenskaber og generelt lavere mutationsomkostninger. Det implementeres via sun.misc. usikker, så det anbefales ikke at bruge denne klasse i et HotSpot Java9 + eller JRockit miljø
  • NonBlockingHashMapLong synes godt om NonBlockingHashMap men bruger primitiv lang nøgler
  • NonBlockingHashSet en simpel indpakning NonBlockingHashMapligesom JDK's java.util.Collections.newSetFromMap ()
  • NonBlockingIdentityHashMap synes godt om NonBlockingHashMap men sammenligner nøgler efter identitet.
  • NonBlockingSetIntet bit-vektorsæt med flere gevind implementeret som en række primitive længes. Arbejder ineffektivt i tilfælde af lydløs autoboxing

6. Test af ydeevne

Lad os bruge JMH til at sammenligne JDK'erne ArrayBlockingQueue mod JCTools køens præstation. JMH er en open-source mikro-benchmark-ramme fra Sun / Oracle JVM-guruer, der beskytter os mod ubestemmelighed af compiler / jvm-optimeringsalgoritmer). Du er velkommen til at få flere detaljer om det i denne artikel.

Bemærk, at kodestykket nedenfor savner et par udsagn for at forbedre læsbarheden. Find den komplette kildekode på GitHub:

offentlig klasse MpmcBenchmark {@Param ({PARAM_UNSAFE, PARAM_AFU, PARAM_JDK}) offentlig ustabil implementering af streng; offentlig flygtig kø; @Benchmark @Group (GROUP_NAME) @GroupThreads (PRODUCER_THREADS_NUMBER) public void write (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement &&! Queue.offer (1L)) {// bevidst efterladt tom}} @Benchmark @ Group (GROUP_NAME) @GroupThreads (CONSUMER_THREADS_NUMBER) public void read (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement && queue.poll () == null) {// forsætligt tomt}}}

Resultater (uddrag for den 95. percentil, nanosekunder pr. Operation):

MpmcBenchmark.MyGroup: MyGroup · p0.95 MpmcArrayQueue sample 1052.000 ns / op MpmcBenchmark.MyGroup: MyGroup · p0.95 MpmcAtomicArrayQueue sample 1106.000 ns / op MpmcBenchmark.MyGroup: MyGroupB64 nøgle · p0.95

Det kan vi seMpmcArrayQueue fungerer bare lidt bedre end MpmcAtomicArrayQueue og ArrayBlockingQueue er langsommere med en faktor på to.

7. Ulemper ved brug af JCTools

Brug af JCTools har en vigtig ulempe - det er ikke muligt at håndhæve, at biblioteksklasserne bruges korrekt. Overvej f.eks. En situation, hvor vi begynder at bruge MpscArrayQueue i vores store og modne projekt (bemærk, at der skal være en enkelt forbruger).

Desværre, da projektet er stort, er der en mulighed for, at nogen laver en programmerings- eller konfigurationsfejl, og køen læses nu fra mere end en tråd. Systemet ser ud til at fungere som før, men nu er der en chance for, at forbrugerne går glip af nogle beskeder. Det er et reelt problem, som måske har stor indflydelse og er meget svært at debugge.

Ideelt set skulle det være muligt at køre et system med en bestemt systemegenskab, der tvinger JCTools til at sikre trådadgangspolitik. F.eks. lokale / test / iscenesættelsesmiljøer (men ikke produktion) kan have det tændt. Desværre leverer JCTools ikke sådan en ejendom.

En anden overvejelse er, at selvom vi sørgede for, at JCTools er betydeligt hurtigere end JDK's modstykke, betyder det ikke, at vores applikation opnår den samme hastighed, når vi begynder at bruge de tilpassede køimplementeringer. De fleste applikationer udveksler ikke mange objekter mellem tråde og er for det meste I / O-bundet.

8. Konklusion

Vi har nu en grundlæggende forståelse af de brugsklasser, der tilbydes af JCTools, og så, hvor godt de klarer sig sammenlignet med JDKs kolleger under tung belastning.

Afslutningsvis, Det er kun værd at bruge biblioteket, hvis vi udveksler mange objekter mellem tråde, og selv da er det nødvendigt at være meget forsigtig med at bevare trådadgangspolitikken.

Som altid kan den fulde kildekode til eksemplerne ovenfor findes på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found