16. Vlßkna (threads)
Java umo╛≥uje tzv. multithreading neboli paralelnφ b∞h dvou Φi vφce
Φßstφ programu. TΘto vlastnosti vyu╛ije nap°φklad Internetov² prohlφ╛eΦ -
m∙╛e nahrßvat obrßzky po sφti, zßrove≥ formßtovat a zobrazovat WWW strßnku
a je╣t∞ k tomu spou╣t∞t applet - nebo textov² editor, kter² m∙╛e provßd∞t
kontrolu pravopisu, zatφmco u╛ivatel pφ╣e dokument apod.
Ka╛dß paraleln∞ b∞╛φcφ Φßst programu se v Jav∞ naz²vß vlßkno (thread). D∙le╛itΘ je, ╛e jednotlivß vlßkna lze naprogramovat tΘm∞°
nezßvisle a pouze v p°φpad∞, ╛e sdφlφ spoleΦnß data nebo pou╛φvajφ stejnΘ
prost°edky (za°φzenφ), je t°eba zajistit jejich °ßdnou synchronizaci
(viz 16.5.). Na v∞t╣in∞ dne╣nφch poΦφtaΦ∙ s jednφm procesorem se samoz°ejm∞ nejednß
o fyzicky souΦasn² b∞h vlßken, ale jednotlivß vlßkna se na procesoru st°φdajφ
- podrobnosti viz 16.4..
16.1. T°φda Thread
Ka╛dΘ vlßkno v Jav∞ je instancφ t°φdy Thread (z balφku java.lang) nebo jejφho potomka. Tato t°φda definuje zßkladnφ metody
jako je spu╣t∞nφ, zastavenφ a ukonΦenφ vlßkna.
JednoduchΘ vlßkno se naprogramuje tak, ╛e se vytvo°φ potomek t°φdy Thread, kter² definuje metodu:
public void run()
obsahujφcφ k≤d, kter² bude provßd∞n paraleln∞. Spu╣t∞nφ vlßkna se
provede zavolßnφm metody start() (metoda run() se p°φmo nevolß).
Priklad 16.1. |
/* t°φda definujφcφ vlßkno */
class Vlakno extends Thread { // (1)
Vlakno(String jmeno) { // (2)
super(jmeno); // (3)
}
public void run() { // (4)
for (int i=0; i<5; i++) {
System.out.println("Vlakno: " + getName()); // (5)
yield(); // (6)
}
}
}
/* hlavnφ program */
public class Vlakna {
public static void main(String[] args) {
Vlakno v1 = new Vlakno("v1"); // (7)
Vlakno v2 = new Vlakno("v2"); // (8)
v1.start(); // (9)
v2.start(); // (10)
}
}
Tento program vytvo°φ dv∞ vlßkna v1 (7) a v2 (8),
instance t°φdy Vlakno (1), a spustφ je (9, 10). Tφm se zahßjφ paralelnφ
provßd∞nφ metod run() (4) obou vlßken. Metoda run() simuluje
smysluplnou Φinnost tφm, ╛e 5x vypφ╣e na v²stup jmΘno svΘho vlßkna (5) - bylo
p°edßno jako parametr konstruktoru (7, 8). V²znam metody yield() (6) viz
16.4.. V²stup programu dopadne takto:
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
|
|
Nejd∙le╛it∞j╣φ metody definovanΘ ve t°φd∞ Thread
jsou: (1)
- Ve°ejnΘ konstruktory:
Thread(), Thread(Runnable r), Thread(String s),
Thread(String s, Runnable r),
Thread(ThreadGroup g, Runnable r, String s),
Thread(ThreadGroup g, Runnable r),
Thread(ThreadGroup g, String s) - parametry:
- r - reference na objekt implementujφcφ metodu run()
(viz 16.2.),
- s - °et∞zec reprezentujφcφ jmΘno vlßkna,
- g - skupina, do kterΘ bude vlßkno za°azeno (viz 16.7.).
- public static Thread currentThread() - vracφ referenci na prßv∞
b∞╛φcφ vlßkno,
- public String getName() - vracφ jmΘno vlßkna,
- public int getPriority() - vracφ prioritu
vlßkna (viz 16.4.),
- public void join() - Φekß na ukonΦenφ vlßkna,
- public void resume() - probudφ vlßkno "odstavenΘ" metodou suspend(),
- public void run() - jßdro vlßkna (def. jako prßzdnß metoda),
- public void start() - zahßjφ b∞h vlßkna, tj. provßd∞nφ metody
run(),
- public static void sleep(long ms) - uspφ (doΦasn∞
zastavφ) vlßkno na ms milisekund. Pak jej probudφ a vlßkno m∙╛e
pokraΦuje v b∞hu,
- public setPriority(int priorita) - nastavφ prioritu
vlßkna (viz 16.4.),
- public void stop() - ukonΦφ vlßkno,
- public void suspend() - "odstavφ" vlßkno. OdstavenΘ vlßkno m∙╛e
probudit pouze metoda resume(),
- public static void yield() - umo╛nφ b∞h jinΘho vlßkna (viz
16.4.).
16.2. Rozhranφ Runnable
Rozhranφ Runnable (z balφku java.lang) obsahuje pouze deklaraci
metody:
public void run();
T°φda pomocφ tohoto rozhranφ m∙╛e definovat jßdro vlßkna, metodu
run(), p°iΦem╛ sama nemusφ b²t potomkem t°φdy Thread. To
je u╛iteΦnΘ zejmΘna v p°φpad∞, ╛e:
- program bude pot°ebovat pouze jedno dal╣φ vlßkno (krom∞ hlavnφho
programu) - je zbyteΦnΘ vytvß°et kv∙li jednΘ instanci vlßkna t°φdu,
- danß t°φda je potomkem n∞jakΘ t°φdy (p°φpad appletu) a pot°ebuje mφt
vlastnosti vlßkna.
Priklad 16.2. |
public class Animace extends java.applet.Applet
implements Runnable {
Thread animator = null;
int xpos = 0;
public void init() {
animator = new Thread(this); // (1)
animator.start();
}
public void run() { // (2)
while(animator != null) { // (3)
repaint(); // (4)
try {
Thread.sleep(80); // (5)
} catch(InterruptedException e) {}
}
}
public void paint(java.awt.Graphics g) { // (6)
g.drawRect(xpos, 0, 20, 20);
xpos = (xpos+1) % 100;
}
}
Applet mß na pozadφ p°ehrßvat animaci. Vytvo°φ proto vlßkno
(1) (viz str.16)
konstruktorem Thread(Runnable r), kterΘmu p°edß odkaz na sebe - tj. na instanci
implementujφcφ rozhranφ Runnable (2). Metoda run() vykreslφ fßzi animace - volß metodu repaint() (4),
poΦkß (uspφ se) na 80 ms (5), a cyklus se opakuje, Φφm╛ vznikß animace -
vykreslovßnφ zaji╣╗uje metoda paint() (6) volanß prohlφ╛eΦem na
╛ßdost metody repaint() (4).
|
|
16.3. Ze ╛ivota vlßkna
Ka╛dΘ vlßkno se v danΘm okam╛iku nachßzφ v prßv∞ jednom z t∞chto stav∙:
- NovΘ vlßkno (new thread) - je stav, kdy je
vlßkno vytvo°eno, ale je╣t∞ nebylo spu╣t∞no (nejsou je╣t∞ alokovßny
systΘmovΘ prost°edky vlßkna).
- "B∞huschopn²" stav (runnable) - je stav po
spu╣t∞nφ metodou start()
. V tomto stavu se m∙╛e nachßzet
vφce spu╣t∞n²ch vlßken, z nich╛ ale jen jedno je (na poΦφtaΦi s jednφm
procesorem) prßv∞ b∞╛φcφ.
- "Neb∞huschopn²" stav (not runnable) - do
tohoto stavu se vlßkno dostane, pokud:
- je uspßno metodou sleep(),
- je "odstaveno" metodou suspend(),
- Φekß v metod∞ wait() (viz 16.5.3.),
- Φekß na vstupnφ/v²stupnφ za°φzenφ.
- MrtvΘ vlßkno (dead thread) - je stav po ukonΦenφ metody
run() nebo po zavolßnφ metody stop().
P°echody mezi jednotliv²mi stavy znßzor≥uje obrßzek:
16.4. Plßnovßnφ (scheduling)
Na poΦφtaΦi s jednφm procesorem, kter² nepodporuje paralelnφ b∞h instrukcφ, se
multithreading simuluje tak, ╛e se vlßkna o procesor d∞lφ,
podle pravidel, kterß urΦuje plßnovaΦ (scheduler).Algoritm∙ pro plßnovßnφ je celß °ada. Java pou╛φvß plßnovßnφ podle
priority: ka╛dΘ vlßkno mß p°id∞leno Φφslo, prioritu, v rozmezφ
konstant MIN_PRIORITY a╛ MAX_PRIORITY (definovanΘ ve
t°φd∞ Thread), kde vy╣╣φ Φφslo znamenß vy╣╣φ prioritu. Od priority se
odvφjφ tato pravidla:
- B∞╛φcφ vlßkno musφ mφt nejvy╣╣φ prioritu.
- Pokud je v b∞huschopnΘm stavu vlßkno s vy╣╣φ prioritou ne╛ mß
vlßkno b∞╛φcφ, je b∞╛φcφ vlßkno tφmto okam╛it∞ vyst°φdßno.
- Vlßkno s ni╛╣φ prioritou m∙╛e b∞╛φcφ vlßkno vyst°φdat jen tehdy, pokud
je b∞╛φcφ vlßkno v neb∞huschopnΘm stavu nebo je ukonΦeno, a zßrove≥
na p°id∞lenφ procesoru neΦekß jinΘ vlßkno s vy╣╣φ prioritou.
- Pokud na p°id∞lenφ procesoru Φekß vφce vlßken se stejnou prioritou, je
dal╣φ vybrßno tak, aby se postupn∞ vyst°φdala v╣echna.
- B∞╛φcφ vlßkno m∙╛e navφc dobrovoln∞ poskytnout procesor jin²m vlßkn∙m se
stejnou prioritou zavolßnφm metody yield().
Z uveden²ch pravidel tedy vypl²vß, ╛e b∞╛φcφ vlßkno nem∙╛e b²t "donuceno"
k vyst°φdßnφ jin²m vlßknem se stejnou nebo ni╛╣φ prioritou, ale pouze
s prioritou vy╣╣φ. Java je ov╣em v tomto ohledu benevolentnφ a dovoluje na operaΦnφch systΘmech,
kterΘ to podporujφ (Unix, Windows95/NT), tzv. sdφlenφ Φasu (time
slicing), kdy se vlßkna se stejnou prioritou st°φdajφ po pevn∞
p°id∞len²ch Φasov²ch intervalech. Tφm se zamezφ tomu, aby si nap°φklad vlßkno
s maximßlnφ prioritou (MAX_PRIORITY) zabralo procesor "na v∞ΦnΘ
Φasy".
Priklad 16.3. |
Pokud z p°φkladu 16.1. odstranφme °ßdku (6) s metodou yield(), dopadne v²stup na r∙zn²ch operaΦnφch systΘmech takto:
OS s p°id∞lovßnφm Φasu: OS bez p°id∞lovßnφm Φasu:
Vlakno: v2 Vlakno: v1
Vlakno: v1 Vlakno: v1
Vlakno: v2 Vlakno: v1
Vlakno: v2 Vlakno: v1
Vlakno: v2 Vlakno: v1
Vlakno: v2 Vlakno: v2
Vlakno: v1 Vlakno: v2
Vlakno: v1 Vlakno: v2
Vlakno: v1 Vlakno: v2
Vlakno: v1 Vlakno: v2
Metoda yield() toti╛ zaruΦuje, ╛e se vlßkna mohou spravedliv∞
st°φdat, a tak i v operaΦnφm systΘmu bez p°id∞lovßnφ Φasu vypadß v²stup, jak
bylo uvedeno v p°φkladu 16.1.. Proto╛e sdφlenφ Φasu nenφ v Jav∞ zaruΦeno, nedoporuΦuje se psßt programy,
kterΘ jsou na n∞m zßvislΘ.
|
|
16.5. Synchronizace
Pot°eba synchronizace vznikß v╣ude tam, kde je mo╛nΘ (av╣ak nep°φpustnΘ)
pou╛φvat souΦasn∞ za°φzenφ (nap°. automat na kßvu) nebo sdφlet spoleΦnß data
(nap°. skripta na anal²zu).U automatu je nutnΘ synchronizovat p°φstup lidφ, nebo╗ se nesmφ stßt, aby po
vhozenφ mince byl Φlov∞k odstrΦen jin²m "u╛ivatelem", kter² by pak obdr╛el
k²╛en² nßpoj. Podobn∞ skripta nem∙╛e Φφst vφce student∙ souΦasn∞, jinak by p°i
obracenφ dvou strßnek opaΦn²m sm∞rem do╣lo brzy k jejich destrukci.
16.5.1. KritickΘ sekce
V programovßnφ se Φßsti k≤du, jejich╛ paralelnφ b∞h m∙╛e vyvolat
kolizi, naz²vajφ kritickΘ sekce. Provßd∞nφ kritick²ch sekcφ je Φasov∞
zßvislΘ a pokud nejsou synchronizovßny, program "n∞kdy" nepracuje sprßvn∞.
Nejjednodu╣╣φm p°φpadem kritickΘ sekce v Jav∞ je metoda:
Priklad 16.4. |
Souborov² server musφ synchronizovat p°φstup k soubor∙m na
disku - dva u╛ivatelΘ nesmφ zapisovat souΦasn∞ do jednoho souboru a
nep°φpustnΘ je takΘ zapisovat do souboru, kter² je prßv∞ Φten.
class Soubor {
private byte[] disk = {0,0}; // (1)
public void zapis(byte a, byte b) { // (2)
disk[0] = a; // (3)
disk[1] = b; // (4)
}
public byte[] cti() { // (5)
return new int[] {disk[0], disk[1]}; // (6)
}
}
Instance t°φdy Soubor (nazveme ji soubor) bude
p°edstavovat fyzick² soubor, jeho╛ dvoubytov² obsah (1) je mo╛nΘ Φφst (5)
a zapisovat (2). K tomuto souboru budou p°istupovat dv∞ vlßkna A a B -
bude-li do souboru vlßkno A zapisovat:
soubor.zapis(1,2);
a souΦasn∞ vlßkno B z n∞j Φφst:
byte[] data = soubor.cti();
m∙╛e se stßt, ╛e prom∞nnß data bude nakonec mφt obsah
{1,0} namφsto oΦekßvanΘho {1,2}. K p°i°azenφ prom∞nnΘ
disk toti╛ dojde v dob∞ "mezi" (3) a (4). Metody (2) a (5) jsou
kritickΘ sekce.
|
|
KritickΘ sekce jsou v╛dy spojeny se spoleΦn²mi daty nebo za°φzenφm.
Principißln∞ nic nebrßnφ souΦasnΘmu b∞hu dvou kritick²ch sekcφ, kterΘ nesdφlφ data a pou╛φvajφ rozdφlnß za°φzenφ.
16.5.2. Synchronizace kritick²ch sekcφ
VylouΦenφ souΦasnΘho b∞hu kritick²ch sekcφ lze provΘst nap°φklad pomocφ
tzv. monitor∙. Monitor se obecn∞ sklßdß z dat a funkcφ (metod)
jako zßmk∙ (locks) nad daty. V Jav∞ mß ka╛d² objekt vlastnφ
jedineΦn² monitor.P°i vstupu do synchronizovanΘ kritickΘ sekce vlßkno zφskß
monitor (zßmek je uzamΦen) a po jejφm opu╣t∞nφ vlßkno monitor
uvolnφ (zßmek je odemΦen). Po zφskßnφ monitoru jednφm vlßknem
nem∙╛e jinΘ vlßkno zahßjit provßd∞nφ ╛ßdnΘ synchronizovanΘ kritickΘ sekce
nßle╛φcφ k tΘmu╛ monitoru (vlßkno, kterΘ monitor vlastnφ, ano - monitory
jsou reentrantnφ). Synchronizovanou kritickou sekcφ v Jav∞ m∙╛e b²t blok (viz 16.5.4.)
nebo metoda. Synchronizovanß metoda se v programu oznaΦφ modifikßtorem
synchronized. Ka╛dß synchronizovanß sekce je v╛dy provedena jako
ned∞liteln² (atomick²) celek, bez mo╛nosti p°eru╣enφ.
Priklad 16.5. |
Metody (2) a (5) z p°φkladu 16.4. je pro sprßvnou funkci t°eba deklarovat takto:
synchronized public void zapis(byte a, byte b)
synchronized public byte[] cti()
Potom p°i libovolnΘ sekvenci zßpis∙ a Φtenφ nedojde k zapsßnφ ani
naΦtenφ nekonzistentnφho obsahu souboru (prom∞nnΘ disk).
|
|
16.5.3. ┌loha producent - konzument
Komplikovan∞j╣φ p°φpad synchronizce nastßvß pokud dochßzφ mezi
vlßkny k p°edßvßnφ dat. Krom∞ oznaΦenφ kritick²ch sekcφ pomocφ
synchronized se pou╛φvajφ metody:
- public final void wait() - zp∙sobφ zastavenφ vlßkna do
probuzenφ metodou notify() nebo notifyAll() operujφcφ se
stejn²m monitorem. D∙le╛itΘ je, b∞hem Φekßnφ v metod∞ wait() je
monitor doΦasn∞ uvoln∞n a m∙╛e tudφ╛ b²t spu╣t∞na jinß
kritickß sekce v tΘm╛e objektu - to umo╛≥uje zabrßnit zablokovßnφ vlßken (deadlock). (2) Dal╣φ verze tΘto metody umo╛≥ujφ
specifikovat dobu Φekßnφ (timeout):
public final void wait(long ms), public final void wait(long ms,
int ns) (ms - poΦet milisekund, ns - poΦet nanosekund).
- public final void notifyAll() - probudφ vlßkna, kterß Φekajφ
v metod∞ wait() tΘho╛ objektu a tato pak "soupe°φ" o pokraΦovßnφ
b∞hu (kritickΘ sekce) na zßklad∞ priority (viz 16.4.).
- public final void notify() - funguje jako notifyAll(), ale probudφ jen jedno
vlßkno (nenφ zaruΦeno, kterΘ).
Tyto metody jsou definovßny ve t°φd∞ Object (viz
11.5.) a mohou b²t volßny pouze v synchronizovan²ch kritick²ch
sekcφch.
Priklad 16.6. |
Pokud by soubor z p°φkladu 16.4. slou╛il jako (jednomφstnß) fronta, kam by vlßkna
umis╗ovala ("produkovala") soubory urΦenΘ k tisku - metodou zapis()
(2) - a tiskßrna si je postupn∞ vyzvedßvala ("konzumovala") - metodou
cti() (5) - m∙╛e se stßt, ╛e soubor bude vyti╣t∞n vφckrßt nebo naopak
nebude vyti╣t∞n v∙bec: nenφ toti╛ nijak zaruΦena sekvence: zßpis - vybrßnφ - zßpis
- vybrßnφ, ale m∙╛e dojφt nap°φklad ke t°em zßpis∙m po sob∞ (p°iΦem╛ prvnφ
dva soubory se samoz°ejm∞ ztratφ). T°φdu Soubor je pro tento ·Φel t°eba upravit takto:
class Soubor {
private byte[] disk = {0,0};
private boolean volno = true; // (1)
synchronized public void zapis(byte a, byte b) { // (2)
while (!volno) try { // (3)
wait(); // (4)
} catch(InterruptedException e) {}
disk[0] = a; // (5)
disk[1] = b; // (6)
volno = false; // (8)
notifyAll(); // (9)
}
synchronized public byte[] cti() { // (10)
while (!volno) try {
wait(); // (11)
} catch(InterruptedException e) {}
volno = true;
notifyAll(); // (12)
return new int[] {disk[0], disk[1]};
}
}
KlφΦovΘ jsou zde metody wait() (4,11) a notifyAll() (9,12) a
p°φznak volno (1), kter² indikuje zda je fronta volnß (true) nebo
ne (false). Metoda zapis() (2) nejprve ov∞°φ, ╛e fronta je prßzdnß (3) a pokud ne,
Φekß v metod∞ wait() (4). V opaΦnΘm p°φpad∞ zapφ╣e data do souboru
(5,6), nastavφ p°φznak (8) a oznßmφ ostatnφm vlßkn∙m (tiskßrn∞) ukonΦenφ
operace - metodou notifyAll() (9), kterß probudφ ostatnφ vlßkna
Φekajφcφ v metod∞ wait() (4,11) na uvoln∞nφ monitoru. Metoda cti() (10) funguje analogicky.
|
|
16.5.4. Blok synchronized
Tento blok umo╛≥uje oznaΦit jako kritickou sekci men╣φ Φßst ne╛ je metoda
a urΦit, objekt podle jeho╛ monitoru se bude provßd∞t synchronizace.
SychronizovanΘ metody toti╛ implicitn∞ pou╛φvajφ monitor instance, v jejφ╛
t°φd∞ jsou definovßny, co╛ n∞kdy nemusφ vyhovovat. Syntaxe bloku je:
synchronized ( referenceNaObjekt ) {
// kritickß sekce
}
16.6. DΘmoni
DΘmon je vlßkno, kterΘ "pouze" poskytuje slu╛by ostatnφm vlßkn∙m.
V²znam dΘmon∙ je ten, ╛e program, ve kterΘm b∞╛φ pouze vlßkna typu dΘmon, m∙╛e
b²t ukonΦen (viz 5.3.).Z hlediska programu je dΘmon vlßkno, kterΘ mß nastaveno atribut
dΘmona metodou:
public void setDaemon(boolean isDaemon)
s parametrem true. Obdobnß metoda:
public boolean isDaemon()
vracφ true, je-li vlßkno dΘmon; jinak false.
16.7. Skupiny
Skupina (group) umo╛≥uje manipulaci s vφce vlßkny najednou
(spu╣t∞nφ, ukonΦenφ atd.). Vlßkno
m∙╛e b²t p°i inicializaci konstruktorem (viz 16.1.) za°azeno do vlastnφ skupiny - jinak se automaticky stßvß Φlenem skupiny implicitnφ
(default group).Skupiny jsou tvo°eny instancemi t°φdy java.lang.ThreadGroup,
kterß definuje tyto
zßkladnφ metody: (3)
- Konstruktory skupiny:
ThreadGroup(String jmΘno),
ThreadGroup(String jmΘno,
ThreadGroup rodSkupina) - parametry:
- jmΘno - udßvß jmΘno skupiny,
- rodSkupina - udßvß rodiΦovskou skupinu - skupiny mohou
obsahovat dal╣φ (pod)skupiny,
- public int activeCount() - vracφ poΦet vlßken ve skupin∞,
- public String getName() - vracφ jmΘno skupiny,
- public int getMaxPriority() - vracφ maximßlnφ prioritu, kterou
m∙╛e mφt nov∞ p°idßvanΘ vlßkno,
- public resume() - probudφ v╣echna vlßkna ve skupin∞ odstavenß
metodou suspend(),
- public void setName(String jmΘno) - nastavφ jmΘno skupiny,
- public void setMaxPriority(int priorita) - nastavφ maximßlnφ
prioritu, kterou m∙╛e mφt nov∞ p°idßvanΘ vlßkno,
- public stop() - ukonΦφ v╣echna vlßkna ve skupin∞,
- public suspend() - "odstavφ" v╣echna vlßkna ve skupin∞.
Vlßkno nelze po vytvo°enφ p°esunout do jinΘ skupiny, stejn∞ tak nelze
p°esunout ani podskupinu. Je tomu tak z d∙vodu bezpeΦnosti, nap°φklad vlßkna
ka╛dΘho appletu majφ p°id∞lenou svou zvlß╣tnφ skupinu a nem∙╛e se stßt, aby
applet zφskal kontrolu nad cizφm vlßknem tφm, ╛e ho p°esune do svΘ skupiny.
|