Obslužný program Java Concurrency Utility s JCTools

1. Prehľad

V tomto tutoriáli si predstavíme knižnicu JCTools (Java Concurrency Tools).

Jednoducho povedané, toto poskytuje množstvo užitočných dátových štruktúr vhodných pre prácu v prostredí s viacerými vláknami.

2. Neblokujúce algoritmy

Tradične viacvláknový kód, ktorý pracuje v premenlivom zdieľanom stave, používa zámky aby sa zabezpečila konzistencia údajov a publikácie (zmeny vykonané jedným vláknom, ktoré sú viditeľné pre druhé).

Tento prístup má niekoľko nevýhod:

  • vlákna sa môžu zablokovať pri pokuse o získanie zámku, takže nedôjde k žiadnemu pokroku, kým sa nedokončí činnosť iného vlákna - to účinne zabráni paralelizmu
  • čím ťažšie je tvrdenie o zámke, tým viac času JVM trávi plánovaním vlákien, riadením sporov a radov čakajúcich vlákien a tým menej reálnej práce robí
  • zablokovania sú možné, ak sa jedná o viac ako jeden zámok a sú získané / uvoľnené v nesprávnom poradí
  • je možné nebezpečenstvo inverzie priority - vlákno s vysokou prioritou je uzamknuté v snahe získať zámok držaný vláknom s nízkou prioritou
  • väčšinou sa používajú hrubozrnné zámky, ktoré veľmi škodia paralelizmu - jemnozrnné zamykanie vyžaduje starostlivejšiu konštrukciu, zvyšuje réžiu zamykania a je náchylnejšie na chyby

Alternatívou je použitie a neblokujúci algoritmus, t. j. algoritmus, pri ktorom zlyhanie alebo pozastavenie ktoréhokoľvek vlákna nemôže spôsobiť zlyhanie alebo pozastavenie iného vlákna.

Neblokujúci algoritmus je bez zámkov ak je zaručené, že aspoň jedno zo zapojených vlákien bude postupovať v ľubovoľnom časovom období, to znamená, že počas spracovania nemôže dôjsť k zablokovaniu.

Ďalej tieto algoritmy sú bez čakania ak existuje aj zaručený postup na jedno vlákno.

Tu je blokovanie Stoh príklad z vynikajúcej knihy Java Concurrency in Practice; definuje základný stav:

verejná trieda ConcurrentStack {AtomicReference hore = nový AtomicReference(); súkromná statická trieda Uzol {verejná položka E; verejný uzol ďalej; // štandardný konštruktor}}

A tiež niekoľko metód API:

public void push (položka E) {Node newHead = nový uzol (položka); Uzol oldHead; do {oldHead = top.get (); newHead.next = oldHead; } while (! top.compareAndSet (oldHead, newHead)); } public E pop () {Node oldHead; Uzol newHead; do {oldHead = top.get (); if (oldHead == null) {return null; } newHead = oldHead.next; } while (! top.compareAndSet (oldHead, newHead)); vrátiť oldHead.item; }

Vidíme, že algoritmus používa jemnozrnné pokyny na porovnanie a výmenu (CAS) a je bez zámkov (aj keď volá viac vlákien top.compareAndSet () súčasne je zaručené, že jeden z nich bude úspešný), ale nie bez čakania pretože neexistuje záruka, že CAS bude nakoniec úspešný pre akékoľvek konkrétne vlákno.

3. Závislosť

Najskôr do našej pridajme závislosť JCTools pom.xml:

 org.jctools jctools-core 2.1.2 

Upozorňujeme, že najnovšia dostupná verzia je k dispozícii na serveri Maven Central.

4. Fronty JCTools

Knižnica ponúka množstvo frontov, ktoré sa dajú použiť v prostredí s viacerými vláknami, t. J. Jedno alebo viac vlákien sa zapisuje do frontu a jedno alebo viac vlákien sa z nej číta bezpečným spôsobom bez zámkov.

Spoločné rozhranie pre všetkých Fronta implementácie je org.jctools.queues.MessagePassingQueue.

4.1. Typy front

Všetky rady možno kategorizovať podľa ich politík výrobcov / spotrebiteľov:

  • jediný výrobca, jeden spotrebiteľ - tieto triedy sú pomenované pomocou predpony Spsc, napr. SpscArrayQueue
  • jediný výrobca, viac spotrebiteľov - použitie Spmc predpona, napr. SpmcArrayQueue
  • viac výrobcov, jeden spotrebiteľ - použitie MPSC predpona, napr. MpscArrayQueue
  • viac výrobcov, viac spotrebiteľov - použitie MPMC predpona, napr. MpmcArrayQueue

Je dôležité si to uvedomiť interne neexistujú žiadne kontroly politiky, t. j. fronta by v prípade nesprávneho použitia mohla ticho fungovať.

Napr. nasledujúci test sa naplní a jeden producent poradie z dvoch vlákien a vyhovuje, aj keď spotrebiteľ nemá zaručené, že uvidí údaje od rôznych výrobcov:

Fronta SpscArrayQueue = nová SpscArrayQueue (2); Producent nití1 = nový Thread (() -> queue.offer (1)); producent1.start (); producent1.join (); Producent nití2 = nový Thread (() -> queue.offer (2)); producent2.start (); producer2.join (); Nastaviť fromQueue = new HashSet (); Spotrebiteľ vlákna = nové vlákno (() -> queue.drain (fromQueue :: add)); consumer.start (); consumer.join (); assertThat (fromQueue). obsahuje Iba (1, 2);

4.2. Implementácie do frontu

Ak zhrnieme vyššie uvedené klasifikácie, je tu zoznam frontov JCTools:

  • SpscArrayQueue jediný producent, jeden spotrebiteľ, používa pole interne, viazaná kapacita
  • SpscLinkedQueue jediný výrobca, jeden spotrebiteľ, interne používa prepojený zoznam, neviazaná kapacita
  • SpscChunkedArrayQueue jediný výrobca, jediný spotrebiteľ, začína s počiatočnou kapacitou a rastie až do maximálnej kapacity
  • SpscGrowableArrayQueue jediný výrobca, jediný spotrebiteľ, začína s počiatočnou kapacitou a rastie až do maximálnej kapacity. Toto je rovnaká zmluva ako SpscChunkedArrayQueue, jediný rozdiel je v správe interných blokov. Odporúča sa používať SpscChunkedArrayQueue pretože má zjednodušenú implementáciu
  • SpscUnboundedArrayQueue jediný výrobca, jeden spotrebiteľ, používa pole interne, neviazaná kapacita
  • SpmcArrayQueue jediný producent, viac spotrebiteľov, používa pole interne, viazanú kapacitu
  • MpscArrayQueue viac výrobcov, jeden spotrebiteľ, používa pole interne, viazaná kapacita
  • MpscLinkedQueue viac výrobcov, jeden spotrebiteľ, interne používa prepojený zoznam, neviazaná kapacita
  • MpmcArrayQueue viac výrobcov, viac spotrebiteľov, používa pole interne, viazaná kapacita

4.3. Atómové fronty

Používajú sa všetky fronty uvedené v predchádzajúcej časti slnko.misc.nebezpečný. S príchodom Java 9 a JEP-260 sa však toto API štandardne stáva neprístupným.

Existujú teda alternatívne fronty, ktoré sa používajú java.util.concurrent.atomic.AtomicLongFieldUpdater (verejné API, menej výkonné) namiesto slnko.misc.nebezpečný.

Generujú sa z radov vyššie a ich názvy majú slovo Atómová vložené medzi, napr. SpscChunkedAtomicArrayQueue alebo MpmcAtomicArrayQueue.

Ak je to možné, odporúča sa používať „bežné“ rady a uchýliť sa k tomu AtomicQueues iba v prostrediach kde slnko.misc.nebezpečný je zakázané / neúčinné ako HotSpot Java9 + a JRockit.

4.4. Kapacita

Všetky fronty JCTools môžu mať tiež maximálnu kapacitu alebo môžu byť neviazané. Keď je rad plný a je viazaný kapacitou, prestane prijímať nové prvky.

V nasledujúcom príklade sme:

  • vyplniť rad
  • zabezpečiť, aby potom prestala prijímať nové prvky
  • odčerpajte z neho a zabezpečte, aby bolo možné neskôr pridať ďalšie prvky

Upozorňujeme, že z dôvodu čitateľnosti bolo zrušených niekoľko príkazov kódu. Kompletnú implementáciu nájdete na GitHub:

Fronta SpscChunkedArrayQueue = nová SpscChunkedArrayQueue (8, 16); CountDownLatch startConsuming = nový CountDownLatch (1); CountDownLatch awakeProducer = nový CountDownLatch (1); Producent nití = nový Thread (() -> {IntStream.range (0, queue.capacity ()). ForEach (i -> {assertThat (queue.offer (i)). IsTrue ();}); assertThat (front .offer (queue.capacity ())). isFalse (); startConsuming.countDown (); awakeProducer.await (); assertThat (queue.offer (queue.capacity ())). isTrue ();}); producent.start (); startConsuming.await (); Nastaviť fromQueue = new HashSet (); queue.drain (fromQueue :: add); awakeProducer.countDown (); producent.join (); queue.drain (fromQueue :: add); assertThat (fromQueue) .containsAll (IntStream.range (0, 17) .boxed (). collect (toSet ()));

5. Ostatné dátové štruktúry JCTools

JCTools ponúka tiež niekoľko dátových štruktúr, ktoré nie sú vo fronte.

Všetky z nich sú uvedené nižšie:

  • NonBlockingHashMap bez zámku ConcurrentHashMap alternatíva s vlastnosťami lepšieho škálovania a všeobecne nižšími nákladmi na mutácie. Implementuje sa prostredníctvom slnko.misc.nebezpečný, takže sa neodporúča používať túto triedu v prostredí HotSpot Java9 + alebo JRockit
  • NonBlockingHashMapLong Páči sa mi to NonBlockingHashMap ale používa primitívne dlho kľúče
  • NonBlockingHashSet jednoduchý obal okolo NonBlockingHashMapako JDK java.util.Collections.newSetFromMap ()
  • NonBlockingIdentityHashMap Páči sa mi to NonBlockingHashMap ale porovnáva kľúče podľa identity.
  • NonBlockingSetIntmnohovláknová sada bitových vektorov implementovaná ako pole primitívnych túži. Funguje neefektívne v prípade tichého autoboxu

6. Testovanie výkonu

Použime JMH na porovnanie JDK ArrayBlockingQueue vs. výkon frontu JCTools. JMH je open-source mikro-benchmark framework od Sun / Oracle JVM guru, ktorý nás chráni pred neurčitosťou optimalizačných algoritmov kompilátora / jvm). Neváhajte a podrobnejšie informácie získate v tomto článku.

Upozorňujeme, že nižšie uvedenému útržku kódu chýba niekoľko vyhlásení, aby sa zlepšila čitateľnosť. Kompletný zdrojový kód nájdete na GitHub:

verejná trieda MpmcBenchmark {@Param ({PARAM_UNSAFE, PARAM_AFU, PARAM_JDK}) verejná volatilná implementácia reťazca; verejný nestály front; @Benchmark @Group (GROUP_NAME) @GroupThreads (PRODUCER_THREADS_NUMBER) public void write (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement &&! Queue.offer (1L)) {// zámerne ponechané prázdne}} @Benchmark @ Group (GROUP_NAME) @GroupThreads (CONSUMER_THREADS_NUMBER) public void read (Control control) {// noinspection StatementWithEmptyBody while (! Control.stopMeasurement && queue.poll () == null) {// zámerne ponechané prázdne}}}

Výsledky (výňatok z 95. percentilu, nanosekundy za operáciu):

MpmcBenchmark.MyGroup: MyGroup · p0,95 MpmcArrayQueue vzorka 1052 000 ns / op MpmcBenchmark.MyGroup: MyGroup · p0,95 MpmcAtomicArrayQueue vzorka 1106 000 ns / op MpmcBenchmark.MyGroup: MyGroup · p0,95 ArrayBlockingQueue

To vidímeMpmcArrayQueue vystupuje len o niečo lepšie ako MpmcAtomicArrayQueue a ArrayBlockingQueue je pomalšie dvojnásobne.

7. Nevýhody používania JCTools

Používanie JCTools má dôležitú nevýhodu - nie je možné vynútiť správne používanie tried knižnice. Zvážte napríklad situáciu, keď začneme používať MpscArrayQueue v našom veľkom a zrelom projekte (všimnite si, že musí existovať jeden spotrebiteľ).

Pretože je projekt veľký, existuje možnosť, že niekto urobí chybu pri programovaní alebo konfigurácii a poradie sa teraz číta z viac ako jedného vlákna. Zdá sa, že systém funguje ako predtým, ale teraz existuje šanca, že spotrebiteľom niektoré správy uniknú. Toto je skutočný problém, ktorý môže mať veľký vplyv a je veľmi ťažké ho ladiť.

V ideálnom prípade by malo byť možné spustiť systém s konkrétnou vlastnosťou systému, ktorý núti JCTools zabezpečiť politiku prístupu k vláknam. Napr. môže byť zapnuté miestne / testovacie / fázové prostredie (ale nie produkčné). Je smutné, že spoločnosť JCTools takéto vlastníctvo neposkytuje.

Ďalšou úvahou je, že aj keď sme zaistili, že JCTools je podstatne rýchlejší ako náprotivok JDK, neznamená to, že naša aplikácia získa rovnakú rýchlosť ako začneme používať implementácie vlastných frontov. Väčšina aplikácií si medzi vláknami nevymieňa veľa objektov a sú väčšinou viazané na I / O.

8. Záver

Teraz máme základné znalosti o triedach nástrojov ponúkaných JCTools a videli sme, ako dobre fungujú, v porovnaní s kolegami JDK pod veľkým zaťažením.

Na záver, Knižnicu sa oplatí používať, iba ak medzi vláknami vymieňame veľa objektov, a aj napriek tomu je potrebné veľmi opatrne dodržiavať politiku prístupu k vláknam.

Celý zdrojový kód pre vyššie uvedené vzorky nájdete ako vždy na serveri GitHub.


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