Uslužni program Java Concurrency s JCTools

1. Pregled

U ovom uputstvu predstavit ćemo biblioteku JCTools (Java Concurrency Tools).

Jednostavno rečeno, ovo pruža brojne korisne podatkovne strukture pogodne za rad u okruženju s više niti.

2. Neblokirajući algoritmi

Tradicionalno, kod s više niti koji radi na promjenjivom zajedničkom stanju koristi brave kako bi se osigurala dosljednost podataka i objave (promjene napravljene jednom niti koje su vidljive drugoj).

Ovaj pristup ima niz nedostataka:

  • niti bi se mogle blokirati u pokušaju dobivanja brave, ne napredujući dok se ne završi operacija druge niti - to učinkovito sprječava paralelizam
  • što je spornija brava teža, JVM više vremena provodi baveći se nitima za raspoređivanje, upravljajući svađom i redovima čekanja niti i manje stvarnog posla koji obavlja
  • zastoji su mogući ako je u pitanju više od jedne brave i oni se nabave / puste pogrešnim redoslijedom
  • moguća je opasnost od inverzije prioriteta - nit visokog prioriteta zaključava se u pokušaju da se zaključavanjem pridržava nit niskog prioriteta
  • većinu vremena koriste se grubozrnate brave, što puno šteti paralelizmu - finozrnasto zaključavanje zahtijeva pažljiviji dizajn, povećava zaključavanje iznad glave i sklonije je pogreškama

Alternativa je upotreba a neblokirajući algoritam, tj. algoritam kod kojeg kvar ili suspenzija bilo koje niti ne može uzrokovati kvar ili suspenziju druge niti.

Neblokirajući algoritam je bez zaključavanja ako je zajamčeno da barem jedna od uključenih niti napreduje tijekom proizvoljnog vremenskog razdoblja, tj. zastoji ne mogu nastati tijekom obrade.

Nadalje, ti algoritmi jesu bez čekanja ako postoji i zajamčeni napredak po niti.

Evo neblokiranja Stog primjer iz izvrsne knjige Java Concurrency in Practice; definira osnovno stanje:

javna klasa ConcurrentStack {AtomicReference vrh = nova AtomicReference(); privatni statički čvor klase {javna E stavka; javni čvor sljedeći; // standardni konstruktor}}

I također nekoliko API metoda:

javni void push (E stavka) {Node newHead = novi čvor (stavka); Čvor oldHead; do {oldHead = top.get (); newHead.next = oldHead; } while (! top.compareAndSet (oldHead, newHead)); } public E pop () {Čvor oldHead; Čvor newHead; do {oldHead = top.get (); if (oldHead == null) {return null; } newHead = oldHead.next; } while (! top.compareAndSet (oldHead, newHead)); return oldHead.item; }

Možemo vidjeti da algoritam koristi precizne upute za usporedbu i zamjenu (CAS) i jest bez zaključavanja (čak i ako poziva više niti vrh.compareAndSet () istodobno, jedan od njih zajamčeno je uspješan), ali ne bez čekanja jer ne postoji jamstvo da CAS na kraju uspije za bilo koju određenu nit.

3. Ovisnost

Prvo, dodajmo ovisnost JCTools našoj pom.xml:

 org.jctools jctools-core 2.1.2 

Napominjemo da je najnovija dostupna verzija dostupna na Maven Central.

4. JCTools redovi

Biblioteka nudi brojne redove za korištenje u okruženju s više niti, tj. Jedna ili više niti upisuju u red i jedna ili više niti čitaju se iz nje na način bez zaključavanja.

Zajedničko sučelje za sve Red implementacije je org.jctools.queues.MessagePassingQueue.

4.1. Vrste redova

Svi se redovi mogu kategorizirati prema njihovim pravilima za proizvođače / potrošače:

  • jedan proizvođač, jedan potrošač - takve se klase imenuju pomoću prefiksa Spsc, npr. SpscArrayQueue
  • jedan proizvođač, više potrošača - koristiti Spmc prefiks, na pr. SpmcArrayQueue
  • više proizvođača, jedan potrošač - koristiti Mpsc prefiks, na pr. MpscArrayQueue
  • više proizvođača, više potrošača - koristiti Mpmc prefiks, na pr. MpmcArrayQueue

Važno je to napomenuti interno nema provjere pravila, tj. red bi se mogao tiho pokvariti u slučaju neispravne upotrebe.

Npr. test u nastavku popunjava a jedan proizvođač red iz dvije niti i prolazi iako potrošaču nije zajamčeno da vidi podatke različitih proizvođača:

SpscArrayQueue red = novi SpscArrayQueue (2); Proizvođač niti1 = nova nit (() -> queue.offer (1)); producent1.start (); proizvođač1.join (); Proizvođač niti2 = nova nit (() -> queue.offer (2)); proizvođač2.start (); proizvođač2.join (); Postavi fromQueue = novi HashSet (); Potrošač niti = nova nit (() -> queue.drain (fromQueue :: add)); potrošač.start (); potrošač.join (); assertThat (fromQueue) .containsOnly (1, 2);

4.2. Implementacije u redu čekanja

Sažimajući gornje klasifikacije, evo popisa redova JCTools:

  • SpscArrayQueue pojedinačni proizvođač, pojedinačni potrošač, koristi niz interno, vezanih kapaciteta
  • SpscLinkedQueue pojedinačni proizvođač, pojedinačni potrošač, interno koristi povezani popis, nevezani kapacitet
  • SpscChunkedArrayQueue pojedinačni proizvođač, pojedinačni potrošač, započinje s početnim kapacitetom i raste do maksimalnog kapaciteta
  • SpscGrowableArrayQueue pojedinačni proizvođač, pojedinačni potrošač, započinje s početnim kapacitetom i raste do maksimalnog kapaciteta. Ovo je isti ugovor kao SpscChunkedArrayQueue, jedina razlika je interno upravljanje dijelovima. Preporuča se koristiti SpscChunkedArrayQueue jer ima pojednostavljenu provedbu
  • SpscUnboundedArrayQueue pojedinačni proizvođač, pojedinačni potrošač, koristi niz interno, nevezanih kapaciteta
  • SpmcArrayQueue jedan proizvođač, više potrošača, koristi niz interno, vezanih kapaciteta
  • MpscArrayQueue više proizvođača, jedan potrošač, koristi niz interno, vezanih kapaciteta
  • MpscLinkedQueue više proizvođača, jedan potrošač, interno koristi povezani popis, nevezani kapacitet
  • MpmcArrayQueue više proizvođača, više potrošača, koristi niz interno, vezanog kapaciteta

4.3. Atomski redovi

Svi redovi spomenuti u prethodnom odjeljku koriste se sunce.misc.Nesigurno. Međutim, pojavom Jave 9 i JEP-260 ovaj API prema zadanim postavkama postaje nedostupan.

Dakle, postoje alternativni redovi koji se koriste java.util.concurrent.atomic.AtomicLongFieldUpdater (javni API, manje izveden) umjesto sunce.misc.Nesigurno.

Oni su generirani iz gornjih redova i njihova imena imaju riječ Atomski umetnuto između, na pr. SpscChunkedAtomicArrayQueue ili MpmcAtomicArrayQueue.

Preporučuje se korištenje redovnih redova ako je moguće i pribjegavanje Atomski redovi samo u sredinama gdje sunce.misc.Nesigurno je zabranjen / neučinkovit poput HotSpot Java9 + i JRockit.

4.4. Kapacitet

Svi JCTools redovi također mogu imati maksimalni kapacitet ili biti nevezani. Kad je red pun i ograničen kapacitetom, prestaje prihvaćati nove elemente.

U sljedećem primjeru mi:

  • popuniti red
  • osigurati da nakon toga prestane prihvaćati nove elemente
  • ispraznite ga i osigurajte da je nakon toga moguće dodati više elemenata

Imajte na umu da je nekoliko izjava koda ispušteno zbog čitljivosti. Kompletnu implementaciju možete pronaći na GitHub:

SpscChunkedArrayQueue red = novi SpscChunkedArrayQueue (8, 16); CountDownLatch startConsuming = novo CountDownLatch (1); CountDownLatch awakeProducer = novi CountDownLatch (1); Proizvođač niti = nova nit (() -> {IntStream.range (0, queue.capacity ()). ForEach (i -> {assertThat (queue.offer (i)). IsTrue ();}); assertThat (queue .offer (queue.capacity ())). isFalse (); startConsuming.countDown (); awakeProducer.await (); assertThat (queue.offer (queue.capacity ())). isTrue ();}); producent.start (); startConsuming.await (); Postavi fromQueue = novi HashSet (); queue.drain (fromQueue :: add); awakeProducer.countDown (); producent.join (); queue.drain (fromQueue :: add); assertThat (fromQueue) .containsAll (IntStream.range (0, 17) .boxed (). collect (toSet ()));

5. Ostale JCTools strukture podataka

JCTools nudi i nekoliko struktura podataka koje nisu iz reda čekanja.

Svi su navedeni u nastavku:

  • NonBlockingHashMap bez brave ConcurrentHashMap alternativa s boljim svojstvima skaliranja i općenito nižim troškovima mutacije. Provodi se putem sunce.misc.Nesigurno, pa se ne preporučuje koristiti ovu klasu u HotSpot Java9 + ili JRockit okruženju
  • NonBlockingHashMapLong Kao NonBlockingHashMap ali koristi primitiv dugo tipke
  • NonBlockingHashSet jednostavan omot uokolo NonBlockingHashMappoput JDK-ovih java.util.Collections.newSetFromMap ()
  • NonBlockingIdentityHashMap Kao NonBlockingHashMap ali uspoređuje ključeve prema identitetu.
  • NonBlockingSetIntvišenitni bit-vektorski skup implementiran kao niz primitiva čezne. Djeluje neučinkovito u slučaju tihog automatskog boksanja

6. Ispitivanje performansi

Upotrijebimo JMH za usporedbu JDK-a ArrayBlockingQueue u odnosu na izvedbu reda JCTools. JMH je mikro-referentni okvir otvorenog koda Sun / Oracle JVM gurua koji nas štiti od neodređenosti algoritama za optimizaciju kompajlera / jvm-a). Slobodno potražite više detalja o tome u ovom članku.

Imajte na umu da isječak koda u nastavku propušta nekoliko izjava kako bi se poboljšala čitljivost. Kompletni izvorni kod potražite na GitHub:

javna klasa MpmcBenchmark {@Param ({PARAM_UNSAFE, PARAM_AFU, PARAM_JDK}) javna hlapljiva implementacija niza; javni nestabilni red čekanja; @Benchmark @Group (GROUP_NAME) @GroupThreads (PRODUCER_THREADS_NUMBER) public void write (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement &&! Queue.offer (1L)) {// namjerno ostavljeno prazno}} @Benchmark @ Grupa (GROUP_NAME) @GroupThreads (CONSUMER_THREADS_NUMBER) očitava javnu prazninu (Kontrola kontrole) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement && queue.poll () == null) {// namjerno ostavljeno prazno}}}

Rezultati (izvadak za 95. percentil, nanosekunde po operaciji):

MpmcBenchmark.MyGroup: MyGroup · p0,95 MpmcArrayQueue uzorak 1052,000 ns / op MpmcBenchmark.MyGroup: MyGroup · p0,95 MpmcAtomicArrayQueue uzorak 1106,000 ns / op MpmcBenchmark.MayGroupcking: MyGroup p.

To možemo vidjetiMpmcArrayQueue izvodi samo malo bolje od MpmcAtomicArrayQueue i ArrayBlockingQueue je sporiji za faktor dva.

7. Nedostaci korištenja JCTools-a

Korištenje JCTools ima važan nedostatak - nije moguće prisiliti da se klase knjižnice koriste ispravno. Na primjer, razmotrimo situaciju kada počnemo koristiti MpscArrayQueue u našem velikom i zrelom projektu (imajte na umu da mora postojati jedan potrošač).

Nažalost, budući da je projekt velik, postoji mogućnost da netko pogriješi u programiranju ili u konfiguraciji, a red se sada čita iz više niti. Čini se da sustav radi kao i prije, ali sada postoji šansa da potrošači propuste neke poruke. To je stvaran problem koji bi mogao imati velik utjecaj i koji je vrlo teško otkloniti.

U idealnom slučaju, trebalo bi biti moguće pokrenuti sustav s određenim sistemskim svojstvom koje prisiljava JCTools da osigura politiku pristupa nitima. Npr. lokalno / testno / scensko okruženje (ali ne i produkcijsko) moglo bi ga uključiti. Nažalost, JCTools ne pruža takvu imovinu.

Sljedeće je razmatranje da, iako smo osigurali da je JCTools znatno brži od JDK-ovog kolege, to ne znači da naša aplikacija dobiva jednaku brzinu kao što počinjemo koristiti prilagođene implementacije reda. Većina aplikacija ne razmjenjuje puno objekata između niti i uglavnom su povezane I / O.

8. Zaključak

Sada imamo osnovno razumijevanje klasa korisnosti koje nude JCTools i vidjeli smo koliko su dobri, u usporedbi s JDK-ovim kolegama pod velikim opterećenjem.

U zaključku, vrijedi koristiti knjižnicu samo ako razmijenimo puno objekata između niti, pa čak i tada je potrebno biti vrlo oprezan kako bismo sačuvali politiku pristupa nitima.

Kao i uvijek, puni izvorni kod za gornje uzorke možete pronaći na GitHubu.