1. diel - Multithreading v Jave
V tomto článku si urobíme úvod do multithreadingu v Jave. Budem
predpokladať, že ste zatiaľ multithreading nevyužívali a môžem tak
povedať, že všetky vaše doterajšie programy prebiehali lineárne. Tým
myslím príkaz po príkaze. Prebiehal vždy len jeden príkaz a aby sa mohli
vykonať ďalšie, musel sa tento príkaz dokončiť. Hovoríme o tom, že
prebieha naraz iba jedno vlákno (anglicky Thread - viď. Ďalej). Možno ste o
tom vláknu ani nevedeli, ale je tam a vstupný bod má v nám dobre známej
metóde main()
. Tento prístup je síce najjednoduchšie, ale
často nie najlepší.
Predstavte si situáciu, kedy jedno vlákno napr. Čaká na vstup od užívateľa. V prípade jednovláknového modeli potom čaká celý program! A pretože používatelia sú spravidla pomalší, než náš program, dochádza k nepríjemnému mrhanie procesorovým časom. Naviac je náš program veľmi nestabilná. Ak sa s tým naším jedným vláknom čokoľvek stane (spadne, je zablokované), bude opäť ovplyvnený celý program. Môže tiež nastať situácia, kedy chceme na určitú dobu nejaké vlákno úmyselne pozastaviť. Rozhodne nie je príjemné, keď kvôli tomu musíme pozastaviť celý program.
Všetky tieto problémy ale riešia ... áno, uhádli ste - multithreading.
Multithreading
Program využívajúce multithreading sa skladá z dvoch a viacerých častí (vlákien) a aj keď sa to zo začiatku bude možno zdať ťažké, je vďaka prepracovanej podpore zo strany Javy vytvorenie viacvláknové aplikácie celkom jednoduché. Dá sa povedať, že vlákno je akási samostatne sa vykonávajúci postupnosť príkazov. Vlákna môžeme ľubovoľne tvoriť (= definovať onú postupnosť príkazov) a spravovať.
Vytvorenie vlákna
Prakticky existujú 2 spôsoby na vytvorenie vlákna:
- Poděděním z triedy
Thread
- Implementáciou rozhranie
Runnable
Tieto dva prístupy sú si rovnocenné a záleží na každom programátorovi, ktorý z nich si vyberie. V tomto článku si ukážeme obaja.
Rozhranie Runnable
Rozhranie Runnable
je tzv. Funkcionálna rozhranie. To je
novinka Javy 8 a nie je to nič iné, než rozhranie s jednou abstraktné
metódou. Neskôr si ukážeme, aké to prináša výhody. Avšak zatiaľ pre
nás bude dôležitejšie abstraktné metóda run()
, ktorú toto
rozhranie definuje. Táto metóda je pre oba vyššie uvedené princípy
spoločná a predstavuje práve tú postupnosť príkazov, ktorú neskôr
vlákno vykonáva.
Trieda Thread
Trieda Thread
implementuje rozhranie Runnable
a
teraz pre nás bude veľmi dôležitá, pretože reprezentuje samotné vlákno.
Definuje veľa konstruktoru, z ktorých pre nás ale budú zatiaľ dôležité
len 4:
public Thread() public Thread(String name) public Thread(Runnable target) public Thread(Runnable target, String name)
Ako vidíte, môžeme u vlákna definovať jeho meno. To je veľmi
užitočná vec, ktorá nám môže pomôcť pri ladení programu. Toto meno
potom môžeme ľahko zmeniť pomocou metódy setName(String name)
,
alebo načítať metódou getName()
.
Druhé dva konštruktory využijeme pri vytváraní vlákna implementáciou
rozhrania Runnable
. Najskôr vytvoríme objekt typu
Runnable
, v ktorom implementujeme metódu run()
a pri
vytváraní vlákna tento objekt odovzdáme v konstruktoru. Vlákno si
odovzdaný objekt uloží a pri zavolaní metódy run()
automaticky
zavolá jeho metódu run()
.
Dôležitou metódou tejto triedy je metóda start()
, ktorá je
akýmsi vstupným bodom vlákna. Táto metóda vykoná prípravné práce a
potom zavolá metódu run()
.
Rozšírenie triedy Thread
Konečne sa teda dostávame k samotnému vytvoreniu vlákna. Založíme si
nový projekt a pomenujeme ho, ako nás práve napadne. Teraz vytvoríme triedu
Vlakno
:
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"); } }
Trieda dedí z triedy Thread
a prekrýva jej metódu
run()
. Jediná nová vec je tu metóda sleep()
:
public static void sleep(long millis) throws InterruptedException
, Ktorú definuje trieda Thread
a ktorá pozastaví vlákno, z
ktorého bola volaná, na dobu danú argumentom Milis. Doba sa zadáva v
milisekundách, čo je tisícina sekundy. Teraz sa zameriame na metódu
main()
:
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"); }
Keď teraz spustíme program, bude výstup vyzerať nejako takto:
Konzolová aplikácia
Hlavní vlákno spuštěno
Hlavní vlákno: 0
Vlákno Druhe spuštěno
Vlákno Druhe: 0
Vlákno Druhe: 1
Hlavní vlákno: 1
Vlákno Druhe: 2
Hlavní vlákno: 2
Vlákno Druhe: 3
Vlákno Druhe ukončeno
Hlavní vlákno: 3
Hlavní vlákno ukončeno
Tiež sa ale tento výstup môže líšiť. Je to samozrejme spôsobené tým, že vlákna voči sebe nebeží vždy rovnako. Raz môže byť rýchlejší, či lepšie povedané dostať viac procesorového času jedno vlákno a druhýkrát zas iné. Práve to je na multithreadingu tak zákerné, že nikdy neviete, ako budú vlákna medzi sebou prepínané. Tomuto prepínanie sa hovorí prepínanie kontextu a stará sa o neho samotný operačný systém.
Prepínanie kontextu
Ak sa dve či viac vlákien delia o jeden procesor (alebo lepšie povedané o
jedno jadro), musí nejakým spôsobom dochádzať k prepínanie medzi ich
vykonávaním. Ako som sa už zmienil, o toto prepínanie sa stará operačný
systém. Našťastie ho ale môžeme explicitne ovplyvniť aj my sami. Na
rozhodovanie o tom, ktorému vláknu bude dovolené implementácie (ktorému
bude pridelený procesorový čas) sa využíva priority vlákien. Každé
vlákno má priradenú prioritu reprezentovanú číslom od 1
do
10
. Predvolená hodnota je 5
. Prioritu môžeme
nastaviť metódou setPriority()
, alebo načítať metódou
getPriority()
.
Dá sa povedať, že vlákno s vyššou prioritou má prednosť v realizácii
pred vláknom s nižšou prioritou a kedykoľvek si môže vynútiť jeho
pozastavenie vo svoj prospech (silnejší prežije ). Ako som ale hovoril, o
prepínanie sa stará operačný systém a každý OS môže s vláknami a ich
prioritou nakladať inak. Preto by ste nemali spoliehať len na automatické
prepínanie a snažiť sa aspoň trochu prepínanie strážiť. Napr. platí,
že je potrebné zariadiť, aby sa vlákna sa zhodnú prioritou raz za čas sama
vzdala riadenia. To môžete elegantne spôsobiť statickou metódou
yield()
na triede Thread
, ktorá "vezme" riadenie
aktuálne bežiacemu vláknu a odovzdá ho čakajúcemu vláknu s najvyššou
prioritou.
Implementácia rozhrania Runnable
Druhým spôsobom vytvorenie vlákna je implementácia rozhrania
Runnable
. Ako som už hovoril, toto rozhranie je tzv. Funkcionálne
rozhrania a teda obsahuje len jednu abstraktné metódu. V tomto prípade je to
samozrejme metóda run()
. Urobme si teda malú odbočku k Jave
8.
Funkcionálne rozhranie a lambda výrazy
Funkcionálne rozhranie je novinka Javy 8 a je to také rozhranie, ktoré má
len jednu abstraktné metódu. Zároveň by pre prehľadnosť malo byť
označené anotácií @FunctionalInterface
.
Predstavme si, že chceme napr. Vytvoriť objekt typu
Comparator
. V starších verziách by sme museli postupovať ako
pri tvorbe abstraktné triedy:
Comparator<String> com = new Comparator<String>() { @Override public int compare(String a, String b) { return b.compareTo(a); } };
Sami musíte uznať, že toľko kódu pre tak jednoduchú operáciu nie je príliš výhodné. Prečo vlastne musíme písať ktorú metódu prekrývame, keď rozhranie má len jednu? Našťastie to ale už nie je nutné. Od Javy 8 tak môžeme to isté napísať takto pomocou lambda výrazu:
Comparator<String> com = (String a, String b) -> { return b.compareTo(a); };
Jednoducho uvedieme parametre abstraktné metódy, operátor
->
, a blok kódu (implementáciu abstraktné metódy). Ak je
však v tomto bloku len jeden príkaz, môžeme vypustiť zložené zátvorky aj
príkaz return. Tiež je možné vypustiť dátový typ parametrov. Ten istý
kód tak môže vyzerať takto:
Comparator<String> com = (a, b) -> b.compareTo(a);
Krása nie? A keby bol v zátvorke len jeden parameter, mohli by sme aj tie zátvorky vypustiť.
Pre prípadných záujemcov o viac informácií tu mám link a jeden úžasný a podrobný článok o Jave 8. Pre tých, čo sa s angličtinou moc nekamaráti mám jeden už nie tak dobrý článok
Teraz už teda vieme, čo je to funkcionálne rozhranie a môžeme ľahko
vytvoriť vlákno pomocou implementácie rozhrania Runnable
.
Pamätáte ešte na druhej dva konštruktory triedy Thread? Tie tu využijeme.
Do metódy main()
teraz namiesto vytvorenia triedy
Vlakno
umiestnite tento kód:
Thread mojeVlakno = new Thread(() -> { 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"); }, "Druhe");
Tu by malo byť všetko jasné. Program by sa mal správať rovnako ako pred modifikáciou. Možno sa vám to bude zdať trochu narýchlo riešenie, asi by bolo v tomto prípade lepšie použiť koncept abstraktné triedy namiesto lambda. Ale to už je na vás.
Budem sa na vás tešiť u ďalšej lekcie multithreading v Jave, Multithreading v Jave - Daemon, join a synchronized .
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é 269x (2.42 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java