Lekce 2 - Multithreading v Javě - Daemon, join a synchronized
V minulé lekci, Multithreading v Javě, jsme si udělali stručný úvod do vláknového modelu Javy a osvojili jsme si základy práce s vlákny.
Bavili jsme se také o hlavním vláknu. Možná ale nebylo úplně jasné, proč je vlastně hlavní vlákno hlavním.
Toto vlákno, které bývá označováno za hlavní, je důležité zejména proto, že se automaticky spustí v okamžiku, kdy je spuštěn javovský program. Do té doby, než jsou z něj vytvořena a spuštěna další vlákna, je vlastně hlavní vlákno synonymum pro samotný program. Hlavní vlákno ale nemusí být (ač je to zvykem) ukončeno jako poslední.
Vezměme si třídu Vlakno
a metodu main()
z
minulého dílu:
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"); }
Zaměňme v metodě main příkaz Thread.sleep(750)
za
Thread.sleep(200)
. Když nyní program spustíme, zjistíme, že
druhé vlákno pokračovalo i po ukončení hlavního vlákna. Obecně platí,
že program probíhá, dokud je prováděno alespoň jedno vlákno, které není
označeno jako daemon. A to bez ohledu na to, zda je vlákno hlavní, či
nikoli. To znamená, že počet běžících daemon vláken nemá žádný vliv
na ukončení programu.
Daemon vlákna běží na pozadí programu podobně jako Garbage collection. Běh těchto vláken má smysl pouze za přítomnosti dalších vláken - právě proto dochází k jejich automatickému ukončení. Dobrým příkladem může být např. nějaký timer. Non-daemon vlákna jsou někdy označována jako user threads.
Při vytvoření má každé vlákno hodnotu atributu daemon nastavenou na
false. To můžeme změnit instanční metodou
setDaemon(boolean daemon)
. Ale pozor, tuto metodu lze volat pouze
před spuštěním vlákna. Pokud je pravidlo porušeno, je vyvolána výjimka
IllegalThreadStateException
.
Umístěme tedy před volání metody start()
na instanci
mojeVlakno
v metodě main()
tento kód:
mojeVlakno.setDaemon(true);
Pokud nyní spustíme program, vykoná se jen část běhu vedlejšího vlákna a zpráva o jeho ukončení se nikdy nezobrazí. Je to samozřejmě proto, že vlákno je označeno jako daemon a ve chvíli, kdy skončí běh hlavního vlákna, program skončí.
Komunikace vláken
Až do teď jsme si situaci s plánováním běhu vláken velice
zjednodušovali používáním metody Thread.sleep()
. Když vlákno
provádí pouze triviální operace a poté dlouhou dobu čeká, lze docela
dobře předpovědět, jak bude běh vlákna probíhat. V reálných aplikacích
ale budou vlákna provádět různé výpočty nebo čekat na vstupy a my
nebudeme moci přesně říci, jak dlouho bude danému vláknu trvat jeho
běh.
Naštěstí existuje celá řada důmyslných metod, které nám dovolují
provádět jakousi mezivláknovou komunikaci. Jedná se o pokročilejší téma
spjaté se synchronizací, takže jeho místo bude v našem seriálu až dále.
Již nyní si ale ukážeme dvě, velice praktické metody –
join()
a isAlive()
.
isAlive()
Metoda isAlive()
je instanční metoda třídy
Thread
vracející true
, pokud je vlákno vůči
němuž byla volána stále běžící. V opačném případě vrací
false
. Zkusme navrhnout program tak, aby hlavní vlákno s
využitím metody isAlive()
“počkalo“ na vlákno
vedlejší:
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"); }
Třída Vlakno
zůstává beze změny. Zde asi není co
vysvětlovat. Měl by se zobrazit následující výstup:
Konzolová aplikace
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
Není to špatné, program funguje jak má. My se ale s tímto řešením
nespokojíme Nebyla by to
totiž Java, kdyby nám nenabízela lepší řešení. Tím je druhá ze
zmíněných metod – metoda join()
.
join()
Metodu join()
rovněž obsahují instance třídy
Thread
, její užití je ale komplexnější. Zajišťuje totiž
čekání vlákna, ze kterého byla volána na vlákno vůči kterému byla
volána. Předchozí kód metody main()
by se tak dal zredukovat
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 spuštění se zobrazí identický výstup.
Synchronizace
Obsáhlým tématem multithreadingu je synchronizace. Je to vlastně způsob zajišťující, že v jednom okamžiku bude mít k danému prostředku přístup pouze jedno vlákno. Představme si situaci, kdy více vláken přistupuje k nějaké složité struktuře – např. kolekci. V takovém případě musí existovat způsob, jak zabránit tomu, aby si vlákna vzájemně “lezla do práce“. Dejme tomu, že jedno vlákno bude procházet jeden prvek kolekce po druhém a vypisovat je. Zároveň ale druhé vlákno bude vyrábět a vkládat do kolekce další prvky. Co by se přesně stalo záleží na konkrétním typu kolekce, jisté ale je, že postup by nevedl k očekávanému výsledku. Ba co hůř, výsledek by ani nebyl stejný pro všechny spuštění, takže by ho nebylo možné předvídat.
Zkusme si vytvořit podobný příklad. Několik vláken bude současně po částech vypisovat nějaký text. Pozměňme tedy 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 příkladu vypisuje současně několik vláken zadaný text tak, že
odesílá do konzole jeden znak za druhým. V cyklu metody run navíc voláme
Thread.sleep(1)
– tím simulujeme provádění časově
náročnější operace než je výpis znaku. My už tušíme, že výstup bude
pokaždé jiný a že výpis slov bude pomíchaný. U mě to např. vypadalo
takto:
Konzolová aplikace
ZAKdrohaonjveic smvete
Pokud chcete, zkuste si z cyklu metody run()
odstranit krátké
uspávání vlákna. Výstup pravděpodobně bude stále promíchaný, i když
ne tolik. Je to dáno tím, že neuspávané vlákno zvládne za jedno
přepnutí kontextu vypsat více znaků.
My se ale nyní budeme zabývat tím, jak docílit nepomíchaného výstupu.
Nevyužijeme k tomu hotové řešení v podobě metody println()
s
tím, že ta přeci taky musí být nějak synchronizovaná . Pokud jste do teď četli
pozorně, měli byste být schopni sestavit řešení pomocí metod
Thread.sleep()
, isAlive()
nebo join()
.
Tato řešení by však pravděpodobně byla neefektivní a hlavně zbytečně
složitá a nepřehledná. Z tohoto důvodu nám Java jako obvykle nabízí
komplexní řešení problému a to právě synchronizaci.
Velmi volné přirovnání
Zkusme si představit, že naše vlákna jsou děti v první třídě sedící v kroužku a vyprávějící si o tom, co zažili o víkendu. Jejich učitelka je despota a určila, že mluvit může jen ten, kdo má v ruce jeden konkrétní kamínek. Takže jedno dítě mluví a všechny ostatní mlčí. Když jedno domluví, předá kamínek dítěti nalevo (nebo napravo – je to jedno, ale snažím se být co nejkonkrétnější), to dostane povolení mluvit a mluví. To se opakuje tak dlouho, dokud všechny děti neřeknou co chtějí.
Vícevláknová aplikace využívající synchronizaci funguje až na drobné rozdíly stejně. Tomu kamínku se říká monitor a vlastnit ho může v jeden okamžik pouze jedno vlákno. V praxi je monitor pouze jakýkoli další objekt.
Celá synchronizace je poté realizována dvěma způsoby:
- Uvedením klíčového slova synchronized v deklaraci hlavičky metody. Poté je monitorem objekt s touto metodou. V praxi to znamená, že na jednom objektu může být najednou prováděna pouze jedna jeho synchronizovaná metoda.
- Vytvořením vlastního bloku synchronized a externím uvedením monitoru. Poté se kód chová prakticky stejně jako synchronizovaná metoda. To znamená, že konkrétní synchronizovaný blok může najednou vykonávat pouze jedno vlákno. Výhodnou je větší variabilita (zvolíme vlastní monitor, blok můžeme uvést všude), nevýhodou vyšší úroveň složitosti.
Blok synchronized vypadá takto:
synchronized(monitor) { // Synchronizované příkazy }
U obou případů vlastně mimo jiné děláme z několika příkazů nedělitelnou (atomickou) operaci. To také znamená, že pokud bude vlákno mající monitor čekat, bude zdržovat všechny ostatní vlákna čekající na monitor.
Pokud vlákno narazí na synchronizovaný blok, ale monitor není volný, je zablokováno a zařazeno do fronty na monitor.
Jestli vám něco stále není jasné, nevadí. Synchronizací se totiž budeme zabývat dále, v lekci Multithreading v Javě - Synchronizace v praxi
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 145x (2.27 kB)
Aplikace je včetně zdrojových kódů v jazyce Java