Java egyidejű segédprogram a JCTools alkalmazással

1. Áttekintés

Ebben az oktatóanyagban bemutatjuk a JCTools (Java Concurrency Tools) könyvtárat.

Egyszerűen fogalmazva, ez számos olyan segédprogram adatstruktúrát biztosít, amelyek alkalmasak többszálas környezetben történő munkára.

2. Nem blokkoló algoritmusok

Hagyományosan a többszálas kód, amely egy módosítható megosztott állapotban működik, zárakat használ az adatok konzisztenciájának és publikációinak biztosítása (az egyik szál által végrehajtott, a másik számára látható változtatások).

Ennek a megközelítésnek számos hátránya van:

  • a szálak blokkolódhatnak egy zár megszerzésének kísérlete során, és nem lépnek előre, amíg egy másik szál művelete nem fejeződik be - ez hatékonyan megakadályozza a párhuzamosságot
  • Minél nagyobb a vetélkedés, annál több időt tölt el a JVM a szálak ütemezésével, a versenyek és a várakozó szálak várakozási sorainak kezelésével, és annál kevésbé valós munkát végez
  • holtpontok lehetségesek, ha egynél több zár van érvényben, és hibás sorrendben szerzik / engedik fel őket
  • lehetséges az elsőbbségi inverzió veszélye - egy magas prioritású szál lezárásra kerül, hogy megpróbálja megszerezni a zárat egy alacsony prioritású szál által
  • legtöbbször durva szemcsés zárakat használnak, ami sokat árt a párhuzamosságnak - a finomszemcsés reteszelés körültekintőbb tervezést igényel, növeli a reteszelés rezsijét és hibára hajlamosabb

Alternatív megoldás az a használata nem blokkoló algoritmus, azaz olyan algoritmus, ahol bármely szál meghibásodása vagy felfüggesztése nem okozhat másik szál meghibásodását vagy felfüggesztését.

Nem blokkoló algoritmus zármentes ha az érintett szálak közül legalább egy garantáltan előrehalad egy tetszőleges időtartam alatt, vagyis a feldolgozás során holtpontok nem keletkezhetnek.

Továbbá ezek az algoritmusok várakozás nélküli ha garantált a szálankénti haladás is.

Itt van egy nem blokkoló Kazal példa a kiváló Java Concurrency in Practice könyvből; meghatározza az alapállapotot:

nyilvános osztály ConcurrentStack {AtomicReference top = új AtomicReference(); privát statikus osztály Node {public E item; public Node következő; // standard konstruktor}}

És még néhány API módszer:

public void push (E elem) {Csomópont newHead = új Csomópont (elem); Csomópont oldFej; do {oldHead = top.get (); newHead.next = oldHead; } while (! top.compareAndSet (oldHead, newHead)); } public E pop () {Csomópont oldHead; Csomópont newHead; do {oldHead = top.get (); if (oldHead == null) {return null; } newHead = oldHead.next; } while (! top.compareAndSet (oldHead, newHead)); return oldHead.item; }

Láthatjuk, hogy az algoritmus finomszemcsés összehasonlítási és cserélési (CAS) utasításokat használ, és az zármentes (még akkor is, ha több szál hív top.compareAndSet () egyszerre egyikük garantáltan sikeres lesz), de nem várakozás nélküli mivel nincs garancia arra, hogy a CAS végül sikerrel jár egy adott szál esetében.

3. Függőség

Először adjuk hozzá a JCTools függőséget a sajátunkhoz pom.xml:

 org.jctools jctools-core 2.1.2 

Felhívjuk figyelmét, hogy a legújabb elérhető verzió a Maven Central oldalon érhető el.

4. JCTools Queues

A könyvtár számos várólistát kínál többszálas környezetben történő felhasználásra, azaz egy vagy több szál ír egy sorba, és egy vagy több szál szálbiztos zármentesen olvassa fel belőle.

A közös felület mindenki számára Sor megvalósítások az org.jctools.queues.MessagePassingQueue.

4.1. A várólisták típusai

Minden sor a termelői / fogyasztói politikájuk szerint kategorizálható:

  • egyetlen termelő, egyetlen fogyasztó - az ilyen osztályokat az előtaggal nevezik meg Spsc, például. SpscArrayQueue
  • egyetlen gyártó, több fogyasztó - használat Spmc előtag, pl. SpmcArrayQueue
  • több gyártó, egyetlen fogyasztó - használat Mpsc előtag, pl. MpscArrayQueue
  • több gyártó, több fogyasztó - használat Mpmc előtag, pl. MpmcArrayQueue

Fontos megjegyezni belsőleg nincsenek házirend-ellenőrzések, vagyis helytelen használat esetén a sor néma működésképtelenséget okozhat.

Például. az alábbi teszt kitölti a egytermelő két szálból áll és várakozik, még akkor is, ha a fogyasztó nem garantáltan látja a különböző gyártók adatait:

SpscArrayQueue queue = új SpscArrayQueue (2); Szálgyártó1 = új Szál (() -> sor.ajánlat (1)); producer1.start (); producer1.csatlakozzon (); Szálgyártó2 = új Szál (() -> sor.ajánlat (2)); producer2.start (); producer2.csatlakozzon (); Set fromQueue = új HashSet (); Szálfogyasztó = new Szál (() -> queue.drain (fromQueue :: add)); fogyasztó.indítás (); fogyasztó.csatlakozzon (); assertThat (fromQueue). csak (1, 2) -t tartalmaz;

4.2. Sor megvalósításai

Összefoglalva a fenti osztályozásokat, itt van a JCTools várólisták listája:

  • SpscArrayQueue egyetlen termelő, egyetlen fogyasztó, belsőleg, kötött kapacitást használ
  • SpscLinkedQueue egyetlen termelő, egyetlen fogyasztó, a kapcsolt listát belsőleg, korlátlan kapacitással használja
  • SpscChunkedArrayQueue egyetlen termelő, egyetlen fogyasztó, kezdeti kapacitással indul, és max
  • SpscGrowableArrayQueue egyetlen termelő, egyetlen fogyasztó, kezdeti kapacitással indul, és max. Ez ugyanaz a szerződés, mint SpscChunkedArrayQueue, az egyetlen különbség a belső darabok kezelése. Használata ajánlott SpscChunkedArrayQueue mert egyszerűsített megvalósítású
  • SpscUnboundedArrayQueue egyetlen termelő, egyetlen fogyasztó, belsőleg, kötetlen kapacitást használ
  • SpmcArrayQueue egyetlen gyártó, több fogyasztó, belsőleg, kötött kapacitást használ
  • MpscArrayQueue több gyártó, egyetlen fogyasztó, belsőleg, kötött kapacitást használ
  • MpscLinkedQueue több gyártó, egyetlen fogyasztó, összekapcsolt listát használ belsőleg, korlátlan kapacitással
  • MpmcArrayQueue több gyártó, több fogyasztó belsőleg, kötött kapacitást használ

4.3. Atom sorok

Az előző szakaszban említett összes sor használatos sun.misc.biztonságos. A Java 9 és a JEP-260 megjelenésével azonban ez az API alapértelmezés szerint elérhetetlenné válik.

Tehát vannak alternatív sorok, amelyek használnak java.util.concurrent.atomic.AtomicLongFieldUpdater (nyilvános API, kevésbé teljesítő) helyett sun.misc.biztonságos.

A fenti várólistákból keletkeznek, és a nevükben szerepel a szó Atom közé beillesztve, pl. SpscChunkedAtomicArrayQueue vagy MpmcAtomicArrayQueue.

Javasoljuk, hogy „rendszeres” sorokat használjon, ha lehetséges, és vegye igénybe AtomicQueues csak olyan környezetekben, ahol sun.misc.biztonságos tiltott / hatástalan, mint a HotSpot Java9 + és a JRockit.

4.4. Kapacitás

Lehetséges, hogy az összes JCTools sor maximális kapacitással rendelkezik, vagy nincs kötve. Ha a várólista megtelt és kapacitás köti, akkor nem fogadja el az új elemeket.

A következő példában mi:

  • töltse ki a sort
  • győződjön meg arról, hogy ezután leáll az új elemek elfogadásával
  • szivárogjon ki belőle, és győződjön meg arról, hogy utólag további elemeket lehet hozzáadni

Felhívjuk figyelmét, hogy az olvashatóság kedvéért néhány kódmondatot elvetünk. A teljes megvalósítás megtalálható a GitHub oldalon:

SpscChunkedArrayQueue queue = új SpscChunkedArrayQueue (8, 16); CountDownLatch startConsuming = new CountDownLatch (1); CountDownLatch awakeProducer = new CountDownLatch (1); Téma gyártója = new Téma (() -> {IntStream.range (0, queue.capacity ()). ForEach (i -> {assertThat (queue.offer (i)). IsTrue ();}); assertThat (sor .offer (queue.capacity ())). isFalse (); startConsuming.countDown (); awakeProducer.await (); assertThat (queue.offer (queue.capacity ())). isTrue ();}); producer.start (); startConsuming.await (); Set fromQueue = új HashSet (); queue.drain (fromQueue :: add); awakeProducer.countDown (); producer.csatlakozzon (); queue.drain (fromQueue :: add); assertThat (fromQueue) .containsAll (IntStream.range (0, 17) .boxed (). collect (toSet ()));

5. Egyéb JCTools adatszerkezetek

A JCTools kínál néhány nem várólista adatstruktúrát is.

Az alábbiak mindegyikét felsoroljuk:

  • NonBlockingHashMap zármentes ConcurrentHashMap alternatívája jobb skálázási tulajdonságokkal és általában alacsonyabb mutációs költségekkel. Keresztül valósult meg sun.misc.biztonságos, ezért nem ajánlott ezt az osztályt HotSpot Java9 + vagy JRockit környezetben használni
  • NonBlockingHashMapLong mint NonBlockingHashMap hanem primitívet használ hosszú kulcsok
  • NonBlockingHashSet egy egyszerű burkoló NonBlockingHashMapmint a JDK-k java.util.Collections.newSetFromMap ()
  • NonBlockingIdentityHashMap mint NonBlockingHashMap de a kulcsokat identitás szerint hasonlítja össze.
  • NonBlockingSetInttöbbszálú bit-vektor halmaz, amely primitív tömbként van megvalósítva vágyakozik. Hatékonyan működik csendes autoboxolás esetén

6. Teljesítményvizsgálat

Használjuk a JMH-t a JDK összehasonlításához ArrayBlockingQueue vs. JCTools várakozási sor teljesítménye. A JMH a Sun / Oracle JVM guruk nyílt forráskódú mikro-benchmark keretrendszere, amely megvéd minket a fordító / jvm optimalizáló algoritmusok indeterminizmusától. Kérjük, nyugodtan tájékozódjon erről a cikkben.

Vegye figyelembe, hogy az alábbi kódrészletből hiányzik néhány utasítás az olvashatóság javítása érdekében. Kérjük, keresse meg a teljes forráskódot a GitHubon:

public class MpmcBenchmark {@Param ({PARAM_UNSAFE, PARAM_AFU, PARAM_JDK}) public volatile String implementáció; nyilvános ingadozó várólista; @Benchmark @Group (GROUP_NAME) @GroupThreads (PRODUCER_THREADS_NUMBER) public void write (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement &&! Queue.offer (1L)) {// szándékosan üresen hagyva}} @Benchmark @ Group (GROUP_NAME) @GroupThreads (CONSUMER_THREADS_NUMBER) public void read (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement && queue.poll () == null) {// szándékosan üresen hagyva}}}

Eredmények (kivonat a 95. percentilishez, műveletenként nanoszekundum):

MpmcBenchmark.MyGroup: MyGroup · p0.95 MpmcArrayQueue minta 1052.000 ns / op MpmcBenchmark.MyGroup: MyGroup · p0.95 MpmcAtomicArrayQueue minta 1106.000 ns / op MpmcBenchmark.MyGrue *04.95.MyGroup:04.95.MyGroup:04.95.MyGroup: MyGrue:04.95

Ezt láthatjukMpmcArrayQueue csak valamivel jobban teljesít, mint MpmcAtomicArrayQueue és ArrayBlockingQueue két-szeresével lassabb.

7. A JCTools használatának hátrányai

A JCTools használatának fontos hátránya van - nem lehet kikényszeríteni, hogy a könyvtár osztályait helyesen használják. Vegyünk például egy helyzetet, amikor elkezdjük használni MpscArrayQueue nagy és kiforrott projektünkben (vegye figyelembe, hogy egyetlen fogyasztónak kell lennie).

Sajnos, mivel a projekt nagy, fennáll annak a lehetősége, hogy valaki programozási vagy konfigurációs hibát követ el, és a sor mostantól több szálból is beolvasásra kerül. Úgy tűnik, hogy a rendszer ugyanúgy működik, mint korábban, de most van esély arra, hogy a fogyasztók elmulasszanak néhány üzenetet. Ez egy valódi probléma, amelynek nagy hatása lehet, és nagyon nehéz hibakeresni.

Ideális esetben lehetővé kell tenni egy olyan rendszer futtatását, amely egy adott rendszer tulajdonsággal rendelkezik, amely arra kényszeríti a JCTools-t, hogy biztosítsa a szál hozzáférési házirendjét. Például. a helyi / teszt / staging környezetek (de nem a gyártás) bekapcsolhatják. Sajnos a JCTools nem nyújt ilyen tulajdonságot.

További szempont, hogy annak ellenére, hogy biztosítottuk, hogy a JCTools lényegesen gyorsabb, mint a JDK megfelelője, ez még nem jelenti azt, hogy alkalmazásunk ugyanolyan sebességet nyer, mint amikor elkezdjük használni az egyéni várakozási sor megvalósításokat. A legtöbb alkalmazás nem cserél sok objektumot a szálak között, és többnyire I / O kötésűek.

8. Következtetés

Most már alapvető ismereteink vannak a JCTools által kínált hasznossági osztályokról, és láttuk, mennyire teljesítenek, szemben a JDK nagy terhelés alatt álló társaival.

Következtetésképpen, csak akkor érdemes a könyvtárat használni, ha sok objektumot cserélünk a szálak között, és akkor is nagyon óvatosnak kell lennünk a szál hozzáférési házirendjének megőrzése érdekében.

Mint mindig, a fenti minták teljes forráskódja megtalálható a GitHubon.