čakať a informovať () Metódy v Jave

1. Úvod

V tomto článku sa pozrieme na jeden z najzákladnejších mechanizmov synchronizácie vlákien Java.

Najskôr si povieme niekoľko základných pojmov a metodík týkajúcich sa súbežnosti.

A vyvinieme jednoduchú aplikáciu - kde sa budeme zaoberať problémami so súbežnosťou s cieľom lepšieho porozumenia počkaj () a oznámiť ().

2. Synchronizácia vlákien v Jave

V prostredí s viacerými vláknami sa môže viac vlákien pokúsiť upraviť ten istý prostriedok. Ak nebudú vlákna správne spravované, bude to samozrejme viesť k problémom s konzistenciou.

2.1. Strážené bloky v Jave

Jedným z nástrojov, ktoré môžeme použiť na koordináciu akcií viacerých vlákien v Jave, sú strážené bloky. Takéto bloky pred opätovným spustením vykonajú kontrolu konkrétneho stavu.

Z tohto dôvodu využijeme:

  • Object.wait () - pozastaviť vlákno
  • Object.notify () - prebudiť vlákno

To možno lepšie pochopiť z nasledujúceho diagramu, ktorý zobrazuje životný cyklus a Závit:

Vezmite prosím na vedomie, že existuje veľa spôsobov kontroly tohto životného cyklu; v tomto článku sa však zameriame iba na počkaj () a oznámiť ().

3. The počkaj () Metóda

Jednoducho povedané, keď voláme počkaj () - toto prinúti súčasné vlákno čakať, kým sa nejaké iné vlákno nevyvolá upozorniť () alebo notifyAll () na rovnakom objekte.

Za týmto účelom musí súčasné vlákno vlastniť monitor objektu. Podľa Javadocs sa to môže stať, keď:

  • popravili sme synchronizované inštančná metóda pre daný objekt
  • popravili sme telo a synchronizované blok na danom objekte
  • vykonaním synchronizovaný statický metódy pre objekty typu Trieda

Upozorňujeme, že iba jedno aktívne vlákno môže vlastniť monitor objektu súčasne.

Toto počkaj () metóda má tri preťažené podpisy. Pozrime sa na tieto.

3.1. počkaj ()

The počkaj () metóda spôsobí, že súčasné vlákno bude čakať neurčito, kým sa nevyvolá iné vlákno upozorniť () pre tento objekt resp notifyAll ().

3.2. čakať (dlhý časový limit)

Pomocou tejto metódy môžeme určiť časový limit, po ktorom sa vlákno automaticky prebudí. Vlákno je možné prebudiť pred dosiahnutím časového limitu pomocou upozorniť () alebo notifyAll ().

Všimnite si, že volanie čakať (0) je to isté ako volanie počkaj ().

3.3. čakať (dlhý čas, int nanos)

Toto je ďalší podpis poskytujúci rovnakú funkcionalitu, iba s tým rozdielom, že môžeme poskytnúť vyššiu presnosť.

Celkový časový limit (v nanosekundách) sa počíta ako 1_000_000 * časový limit + nanos.

4. upozorniť () a notifyAll ()

The upozorniť () metóda sa používa na prebudenie vlákien čakajúcich na prístup k monitoru tohto objektu.

Existujú dva spôsoby oznámenia čakajúcich vlákien.

4.1. upozorniť ()

Pre všetky vlákna čakajúce na monitore tohto objektu (pomocou jedného z počkaj () metóda), metóda upozorniť () upozorní ktoréhokoľvek z nich na svojvoľné prebudenie. Výber presne toho, ktoré vlákno sa má zobudiť, je nedeterministické a závisí od implementácie.

Odkedy upozorniť () prebudí jedno náhodné vlákno, ktoré sa dá použiť na implementáciu vzájomne sa vylučujúceho uzamknutia, kde vlákna vykonávajú podobné úlohy, ale vo väčšine prípadov by bolo realizovateľnejšie notifyAll ().

4.2. notifyAll ()

Táto metóda jednoducho prebudí všetky vlákna, ktoré čakajú na monitore tohto objektu.

Prebudené vlákna sa dokončia obvyklým spôsobom - ako každé iné vlákno.

Ale predtým, ako dovolíme, aby pokračovalo v ich vykonávaní, vždy definovať rýchlu kontrolu stavu potrebného na pokračovanie vo vlákne - pretože môžu nastať situácie, kedy sa vlákno prebudilo bez prijatia oznámenia (tento scenár je uvedený nižšie v príklade).

5. Problém so synchronizáciou odosielateľa a príjemcu

Teraz, keď už rozumieme základom, prejdime na jednoduchý OdosielateľPrijímač aplikácia - ktorá bude využívať počkaj () a upozorniť () spôsoby nastavenia synchronizácie medzi nimi:

  • The Odosielateľ má poslať dátový paket do Prijímač
  • The Prijímač nemôže spracovať dátový paket, kým Odosielateľ dokončí jeho odoslanie
  • Podobne Odosielateľ sa nesmie pokúsiť odoslať ďalší paket, pokiaľ Prijímač už spracoval predchádzajúci paket

Najprv vytvorme Údaje trieda, ktorá sa skladá z údajov paket ktoré budú odoslané z Odosielateľ do Prijímač. Použijeme počkaj () a notifyAll () medzi nimi nastavíte synchronizáciu:

public class Data {private String packet; // True, ak má prijímač čakať // False, ak má odosielateľ čakať private boolean transfer = true; public synchronized void send (String packet) {while (! transfer) {try {wait (); } catch (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("Vlákno prerušené", e); }} transfer = false; this.packet = paket; notifyAll (); } verejne synchronizovaný Reťazec receive () {while (transfer) {try {wait (); } catch (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("Vlákno prerušené", e); }} transfer = true; notifyAll (); spätný paket; }}

Poďme si rozdeliť, čo sa tu deje:

  • The paket premenná označuje dáta, ktoré sa prenášajú cez sieť
  • Máme boolovský premenná prevod - ktoré Odosielateľ a Prijímač použije na synchronizáciu:
    • Ak je táto premenná pravda, potom Prijímač by mal čakať na Odosielateľ na odoslanie správy
    • Ak je to nepravdivépotom Odosielateľ by mal čakať na Prijímač správu prijmete
  • The Odosielateľ používa poslať () metóda odosielania údajov do Prijímač:
    • Ak prevod je nepravda, počkáme zavolaním počkaj () na tomto vlákne
    • Ale keď je pravda, prepneme stav, nastavíme našu správu a zavoláme notifyAll () prebudiť ďalšie vlákna, aby určili, že došlo k významnej udalosti, a môžu skontrolovať, či môžu pokračovať v vykonávaní
  • Podobne Prijímač bude používať príjem () metóda:
    • Ak prevod bol nastavený na nepravdivé od Odosielateľ, potom bude len pokračovať, inak zavoláme počkaj () na tomto vlákne
    • Keď je podmienka splnená, prepneme stav, upozorníme všetky čakajúce vlákna na prebudenie a vrátime dátový paket, ktorý bol Prijímač

5.1. Prečo priložiť počkaj () v zatiaľ čo Slučka?

Odkedy upozorniť () a notifyAll () náhodne prebudí vlákna, ktoré čakajú na monitore tohto objektu, nie je vždy dôležité, aby bola podmienka splnená. Niekedy sa môže stať, že sa vlákno prebudí, ale podmienka ešte nie je uspokojená.

Môžeme tiež definovať kontrolu, ktorá nás zachráni pred falošnými prebudeniami - kde sa vlákno môže prebudiť z čakania bez toho, aby niekedy dostalo upozornenie.

5.2. Prečo potrebujeme synchronizáciu skoniec() a príjem () Metódy?

Tieto metódy sme umiestnili dovnútra synchronizované metódy na zabezpečenie vnútorných zámkov. Ak vlákno volá počkaj () metóda nevlastní inherentný zámok, bude vyhodená chyba.

Teraz vytvoríme Odosielateľ a Prijímač a implementovať Spustiteľné rozhrania na oboch, aby ich inštancie bolo možné vykonať pomocou vlákna.

Najprv sa pozrime ako Odosielateľ bude pracovať:

verejná trieda Sender implementuje Runnable {private Data; // štandardné konštruktory public void run () {String packet [] = {"Prvý paket", "Druhý paket", "Tretí paket", "Štvrtý paket", "Koniec"}; pre (String packet: packets) {data.send (packet); // Thread.sleep () na napodobnenie náročného spracovania na strane servera vyskúšajte {Thread.sleep (ThreadLocalRandom.current (). NextInt (1000, 5000)); } catch (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("Vlákno prerušené", e); }}}}

Pre to Odosielateľ:

  • Vytvárame niekoľko náhodných dátových paketov, ktoré budú odoslané cez sieť v balíčky [] pole
  • Pre každý paket iba voláme poslať ()
  • Potom voláme Thread.sleep () s náhodným intervalom na napodobnenie náročného spracovania na strane servera

Nakoniec poďme implementovať naše Prijímač:

prijímač verejnej triedy implementuje Runnable {súkromné ​​načítanie dát; // štandardné konštruktory public void run () {for (String receiveMessage = load.receive ();! "End" .equals (receiveMessage); receiveMessage = load.receive ()) {System.out.println (receiveMessage); // ... try {Thread.sleep (ThreadLocalRandom.current (). nextInt (1000, 5000)); } catch (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("Vlákno prerušené", e); }}}}

Tu jednoducho voláme load.receive () v slučke, kým nezískame posledný "Koniec" dátový paket.

Pozrime sa teraz na túto aplikáciu v akcii:

public static void main (String [] args) {Data data = new Data (); Odosielateľ vlákna = nové vlákno (nový odosielateľ (údaje)); Prijímač vlákien = nový vlákno (nový prijímač (dáta)); sender.start (); receiver.start (); }

Dostaneme nasledujúci výstup:

Prvý paket Druhý paket Tretí paket Štvrtý paket 

A sme tu - dostali sme všetky dátové pakety v správnom poradí a úspešne sme nadviazali správnu komunikáciu medzi našim odosielateľom a príjemcom.

6. Záver

V tomto článku sme diskutovali o niektorých základných konceptoch synchronizácie v prostredí Java; konkrétnejšie sme sa zamerali na to, ako môžeme použiť počkaj () a upozorniť () vyriešiť zaujímavé problémy so synchronizáciou. A nakoniec sme si prešli ukážku kódu, kde sme tieto koncepty aplikovali v praxi.

Predtým, ako sa tu skončíme, stojí za zmienku, že všetky tieto nízkoúrovňové API, ako napr počkaj (), upozorniť () a notifyAll () - sú tradičné metódy, ktoré fungujú dobre, ale mechanizmy na vyššej úrovni sú často jednoduchšie a lepšie - napríklad natívne v jazyku Java Zamknúť a Stav rozhrania (k dispozícii v java.util.concurrent.locks balíček).

Pre viac informácií o java.util.concurrent balíček, navštívte náš prehľad článku java.util.concurrent a Zamknúť a Stav sú obsiahnuté v príručke k java.util.concurrent.Locks, tu.

Kompletné útržky kódu použité v tomto článku sú ako vždy k dispozícii na GitHub.


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