2. diel - Multithreading v Jave - Daemon, join a synchronized
V minulej lekcii, Multithreading v Jave , sme si urobili stručný úvod do vláknového modelu Javy a osvojili sme si základy práce s vláknami. Bavili sme sa aj o hlavnom vláknu. Možno ale nebolo úplne jasné, prečo je vlastne hlavný vlákno hlavným.
Toto vlákno, ktoré býva označované za hlavné, je dôležité najmä preto, že sa automaticky spustí v okamihu, keď je spustený javovský program. Do tej doby, než sú z neho vytvorená a spustená ďalšie vlákna, je vlastne hlavný vlákno synonymum pre samotný program. Hlavné vlákno ale nemusí byť (hoci je to zvykom) ukončené ako posledný.
Zoberme si triedu Vlakno
a metódu main()
z
minulého dielu:
class Vlakno extends Thread { public Vlakno(String jmeno) { super(jmeno); } @Override public void run() { System.out.println("Vlákno " + getName() + " spuštěno"); for(int i = 0; i < 4; ++i) { System.out.println("Vlákno " + getName() + ": " + i); try { Thread.sleep(500); } catch (InterruptedException ex) { System.out.println("Vlákno " + getName() + " přerušeno"); return; } } System.out.println("Vlákno " + getName() + " ukončeno"); } } public static void main(String[] args) { System.out.println("Hlavní vlákno spuštěno"); Vlakno mojeVlakno = new Vlakno("Druhe"); mojeVlakno.start(); for(int i = 0; i < 4; ++i) { System.out.println("Hlavní vlákno: " + i); try { Thread.sleep(750); } catch (InterruptedException ex) { System.out.println("Hlavní vlákno přerušeno"); return; } } System.out.println("Hlavní vlákno ukončeno"); }
Nahraďme v metóde main príkaz Thread.sleep(750)
za
Thread.sleep(200)
. Keď teraz program spustíme, zistíme, že
druhé vlákno pokračovalo aj po ukončení hlavného vlákna. Všeobecne
platí, že program prebieha, kým je vykonávané aspoň jedno vlákno, ktoré
nie je označené ako daemon. A to bez ohľadu na to, či je vlákno hlavné,
alebo nie. To znamená, že počet bežiacich daemon vlákien nemá žiadny
vplyv na ukončenie programu.
Daemon vlákna beží na pozadí programu podobne ako Garbage collection. Beh týchto vlákien má zmysel iba za prítomnosti ďalších vlákien - práve preto dochádza k ich automatickému ukončeniu. Dobrým príkladom môže byť napr. Nejaký timer. Non-daemon vlákna sú niekedy označovaná ako user threads.
Pri vytvorení má každé vlákno hodnotu atribútu daemon nastavenú na
false. To môžeme zmeniť inštančný metódou
setDaemon(boolean daemon)
. Ale pozor, túto metódu možno volať
len pred spustením vlákna. Ak je pravidlo porušené, je vyvolaná výnimka
IllegalThreadStateException
.
Umiestni teda pred volanie metódy start()
na inštanciu
mojeVlakno
v metóde main()
tento kód:
mojeVlakno.setDaemon(true);
Ak teraz spustíme program, vykoná sa len časť behu vedľajšieho vlákna a správa o jeho ukončení sa nikdy nezobrazí. Je to samozrejme preto, že vlákno je označené ako daemon a vo chvíli, kedy sa skončí beh hlavného vlákna, program skončí.
Komunikácia vlákien
Až do teraz sme si situáciu s plánovaním behu vlákien veľmi
zjednodušovali používaním metódy Thread.sleep()
. Keď vlákno
vykonáva len triviálne operácie a potom dlhú dobu čaká, možno celkom
dobre predpovedať, ako bude beh vlákna prebiehať. V reálnych aplikáciách
ale budú vlákna vykonávať rôzne výpočty alebo čakať na vstupy a my
nebudeme môcť presne povedať, ako dlho bude danému vláknu trvať jeho
beh.
Našťastie existuje celá rada dômyselných metód, ktoré nám dovoľujú
vykonávať akúsi mezivláknovou komunikáciu. Jedná sa o pokročilejšie
tému späté so synchronizáciou, takže jeho miesto bude v našom seriáli až
ďalej. Už teraz si však ukážeme dve, veľmi praktické metódy -
join()
a isAlive()
.
IsAlive ()
Metóda isAlive()
je inštančný metóda triedy
Thread
vracajúci true
, ak je vlákno voči ktorému
bola volaná stále bežiaci. V opačnom prípade vracia false
.
Skúsme navrhnúť program tak, aby hlavné vlákno s využitím metódy
isAlive()
"počkalo" na vlákno vedľajšej:
public static void main(String[] args) throws InterruptedException { System.out.println("Hlavní vlákno spuštěno"); Vlakno mojeVlakno = new Vlakno("Druhe"); mojeVlakno.start(); while(mojeVlakno.isAlive()) { Thread.sleep(1); } System.out.println("Hlavní vlákno ukončeno"); }
Trieda Vlakno
zostáva bezo zmeny. Tu asi nie je čo
vysvetľovať. Mal by sa zobraziť nasledujúci výstup:
Konzolová aplikácia
Hlavní vlákno spuštěno
Vlákno Druhe spuštěno
Vlákno Druhe: 0
Vlákno Druhe: 1
Vlákno Druhe: 2
Vlákno Druhe: 3
Vlákno Druhe ukončeno
Hlavní vlákno ukončeno
Nie je to zlé, program funguje ako má. My sa ale s týmto riešením
neuspokojíme : D Nebola by to totiž Java, keby nám neponúkala lepšie
riešenie. Tým je druhá zo spomínaných metód - metóda
join()
.
Join ()
Metódu join()
tiež obsahujú inštancie triedy
Thread
, jej použitie je ale komplexnejšie. Zaisťuje totiž
čakanie vlákna, z ktorého bola volaná na vlákno voči ktorému bola
volaná. Predchádzajúci kód metódy main()
by sa tak dal
zredukovať na:
System.out.println("Hlavní vlákno spuštěno"); Vlakno mojeVlakno = new Vlakno("Druhe"); mojeVlakno.start(); mojeVlakno.join(); System.out.println("Hlavní vlákno ukončeno");
Po spustení sa zobrazí identický výstup.
Synchronizácia
Obsiahlym témou multithreadingu je synchronizácia. Je to vlastne spôsob zaisťujúce, že v jednom okamihu bude mať k danému prostriedku prístup iba jedno vlákno. Predstavme si situáciu, kedy viac vlákien pristupuje k nejakej zložitej štruktúre - napr. Kolekciu. V takom prípade musí existovať spôsob, ako zabrániť tomu, aby si vlákna vzájomne "liezla do práce". Dajme tomu, že jedno vlákno bude prechádzať jeden prvok kolekcie po druhom a vypisovať ich. Zároveň ale druhé vlákno bude vyrábať a vkladať do kolekcie ďalšie prvky. Čo by sa presne stalo záleží na konkrétnom type kolekcie, isté ale je, že postup by neviedol k očakávanému výsledku. Ba čo horšie, výsledok by ani nebol rovnaký pre všetkých spustenie, takže by ho nebolo možné predvídať.
Skúsme si vytvoriť podobný príklad. Niekoľko vlákien bude súčasne po častiach vypisovať nejaký text. Pozmení teda náš kód:
public static void main(String[] args) throws InterruptedException { Vlakno v1 = new Vlakno("Zdravim"); Vlakno v2 = new Vlakno("Ahoj svete"); Vlakno v3 = new Vlakno("Konec"); v1.start(); v2.start(); v3.start(); } static class Vlakno extends Thread { private final String zprava; public Vlakno(String zprava) { this.zprava = zprava; } @Override public void run() { int pozice = 0; while(pozice < zprava.length()) { System.out.print(zprava.charAt(pozice++)); try { Thread.sleep(1); } catch (InterruptedException ex) { System.out.println("Vlákno se zprávou \"" + zprava + "\" přerušeno"); return; } } } }
V tomto príklade vypisuje súčasne niekoľko vlákien zadaný text tak, že
odosiela do konzoly jeden znak za druhým. V cykle metódy run navyše voláme
Thread.sleep(1)
- tým simulujeme vykonávanie časovo
náročnejšie operácie ako je výpis znaku. My už tušíme, že výstup bude
zakaždým iný a že výpis slov bude pomiešaný. U mňa to napr. Vyzeralo
takto:
Konzolová aplikácia
ZAKdrohaonjveic smvete
Ak chcete, skúste si z cyklu metódy run()
odstrániť krátke
uspávanie vlákna. Výstup pravdepodobne bude stále premiešaný, aj keď nie
toľko. Je to dané tým, že neuspávané vlákno zvládne za jedno prepnutie
kontextu vypísať viac znakov.
My sa ale teraz budeme zaoberať tým, ako docieliť nepomíchaného
výstupu. Nevyužijeme k tomu hotové riešenie v podobe metódy
println()
s tým, že tá predsa taky musí byť nejako
synchronizovaná . Ak ste do
teraz čítali pozorne, mali by ste byť schopní zostaviť riešenie pomocou
metód Thread.sleep()
, isAlive()
alebo
join()
. Tieto riešenia by však pravdepodobne bola neefektívna a
hlavne zbytočne zložitá a neprehľadná. Z tohto dôvodu nám Java ako
zvyčajne ponúka komplexné riešenie problému a to práve
synchronizáciu.
Veľmi voľné prirovnanie
Skúsme si predstaviť, že naše vlákna sú deti v prvej triede sediaci v krúžku a rozprávajúcej si o tom, čo zažili cez víkend. Ich učiteľka je despota a určila, že hovoriť môže len ten, kto má v ruke jeden konkrétny kamienok. Takže jedno dieťa hovorí a všetky ostatné mlčí. Keď jedno dohovorí, odovzdá kamienok dieťaťu naľavo (alebo napravo - je to jedno, ale snažím sa byť čo najkonkrétnejšie), to dostane povolenie hovoriť a hovorí. To sa opakuje tak dlho, kým všetky deti nepovedia čo chcú.
Viacvláknové aplikácie využívajúce synchronizáciu funguje až na drobné rozdiely rovnako. Tomu kamienka sa hovorí monitor a vlastniť ho môže v jeden okamih iba jedno vlákno. V praxi je monitor iba akýkoľvek ďalší objekt.
Celá synchronizácia je potom realizovaná dvoma spôsobmi:
- Uvedením kľúčového slova synchronized v deklarácii hlavičky metódy. Potom je monitorom objekt s touto metódou. V praxi to znamená, že na jednom objekte môže byť naraz vykonávať len jedna jeho synchronizovaná metóda.
- Vytvorením vlastného bloku synchronized a externým uvedením monitora. Potom sa kód chová prakticky rovnako ako synchronizovaná metóda. To znamená, že konkrétny synchronizovaný blok môže naraz vykonávať iba jedno vlákno. Výhodnú je väčšia variabilita (zvolíme vlastné monitor, blok môžeme uviesť všade), nevýhodou vyššiu úroveň zložitosti.
Blok synchronized vyzerá takto:
synchronized(monitor) { // Synchronizované příkazy }
Pri oboch prípadoch vlastne okrem iného robíme z niekoľkých príkazov nedeliteľnou (atomické) operáciu. To tiež znamená, že pokiaľ bude vlákno majúce monitor čakať, bude zdržiavať všetky ostatné vlákna čakajúce na monitor.
Ak vlákno narazí na synchronizovaný blok, ale monitor nie je voľný, je zablokované a zaradené do fronty na monitor.
Ak vám niečo stále nie je jasné, nevadí. Synchronizáciou sa totiž budeme zaoberať ďalej, v lekcii Multithreading v Jave - Synchronizácia v praxi
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 145x (2.27 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java