P R O G R A M U J E M EDruhý spôsob spoèíva v implementácii rozhrania Run<strong>na</strong>ble, ktoré obsahuje metódurun(). Objekt implementujúci toto rozhranie potom môeme zada ako argument kon−štruktora <strong>na</strong> vytvorenie objektu typu Thread. Pri tomto spôsobe sa po zavolaní metódystart() zaène vykonáva metóda run() objektu Run<strong>na</strong>ble. Ukáka:class MyThread implements Run<strong>na</strong>ble{// ...}MyThread mt = new MyThread();Thread tt = new Thread(mt);tt.start();Ako vidno, tentoraz vytvárame dva objekty; jeden typu MyThread (to je ten, ktorý vmetóde run() obsahuje kód nového threadu) a druhý typu Thread – ten funguje ako„obálka“ prvého objektu.Pri vytváraní nového threadu môeme volite¾ne zada jeho meno; to sa môe hodi<strong>na</strong>príklad pri ladení projektu. Okrem toho môeme threadu prideli <strong>sk</strong>upinu, do ktorejbude patri (pozri ïalej).Trieda Thread po<strong>sk</strong>ytuje nieko¾ko ve¾mi potrebných metód. Pomocou metódy yield() sa<strong>na</strong>príklad thread môe vzda procesora. Zavolaním metódy sleep() thread <strong>na</strong> stanovený èas„zaspí“. Násilne preruši thread mono metódou interrupt(). Pomocou metódy join()môeme prikáza aktuálnemu threadu, aby èakal <strong>na</strong> <strong>sk</strong>onèenie iného threadu. Metóda isAlive()indikuje, èi daný thread ije, t. j. èi bol spustený a dosia¾ beí. Názov a prioritu thre−adu zisujeme a <strong>na</strong>stavujeme dvojicami (get|set)Name() a (get|set) Priority().Javov<strong>sk</strong>ý program sa konèí buï explicitným volaním System.exit(), alebo v oka−mihu, keï svoj beh <strong>sk</strong>onèia všetky thready. Pomocou metódy setDaemon() môemevybrané thready premeni <strong>na</strong> „démonov“ (a <strong>na</strong>opak). Systém pri rozhodovaní, èi ukonèiprogram, <strong>na</strong> „démonické“ thready neberie oh¾ad. Metóda run() týchto threadov obyèaj−ne obsahuje nekoneènú sluèku. Na zistenie, èi je vybraný thread démonom, pouijememetódu isDaemon().SKUPINY THREADOV. Thready mono zluèova do <strong>sk</strong>upín, ktoré tvoria stromo−vú štruktúru. Kadá <strong>sk</strong>upi<strong>na</strong> okrem poèiatoènej má pridelenú rodièov<strong>sk</strong>ú <strong>sk</strong>upinu. Privytvorení nového threadu mono urèi, do ktorej <strong>sk</strong>upiny bude thread zaradený.Na reprezentáciu <strong>sk</strong>upiny threadov je urèená trieda ThreadGroup. Pri vytvorení jejmono prideli meno a rodièov<strong>sk</strong>ú <strong>sk</strong>upinu. Ak rodièov<strong>sk</strong>ú <strong>sk</strong>upinu nezadáme, stane saòou automaticky <strong>sk</strong>upi<strong>na</strong>, do ktorej patrí aktuálny thread. Pomocou metódy setMax-Priority() <strong>na</strong>stavujeme maximálnu hodnotu priority, akú môu ma thready patriacedo <strong>sk</strong>upiny. Z ïalších metód vyberáme setDaemon() <strong>na</strong> transformáciu všetkých threa−dov v <strong>sk</strong>upine <strong>na</strong> démonov a <strong>na</strong>opak, interrupt() <strong>na</strong> prerušenie všetkých threadov v<strong>sk</strong>upine, getName(), getParent(), getMaxPriority(), isDaemon(), isDestroyed()<strong>na</strong> zistenie atribútov <strong>sk</strong>upiny èi activeCount()/activeGroupCount() <strong>na</strong> ziste−nie poètu aktívnych threadov a aktívnych <strong>sk</strong>upín patriacich do zadanej <strong>sk</strong>upiny.V súvislosti s threadmi spomeòme ešte triedu ThreadLocal. Táto trieda predstavu−je zvláštny druh objektov, líšiacich sa od bených premenných v tom, e kadý thread,ktorý s nimi pracuje, má k dispozícii vlastnú, nezávisle inicializovanú kópiu. ObjektyThreadLocal zvyèajne bývajú privátnymi statickými zlokami tried, ktoré s threadminejakým spôsobom súvisia.Na èítanie a zápis hodnoty premenných typu ThreadLocal pouijeme metódyget() a set(), ktoré pracujú s typom Object, take uloená hodnota môe by vpodstate ¾ubovo¾ná. Poèiatoèná hodnota premenných je null; ak potrebujeme poui inúinicializaènú hodnotu, musíme si odvodi vlastnú triedu a predefinova chránenú metó−du initialValue().SYNCHRONIZÁCIA. Nevyhnutným dôsledkom zavedenia multita<strong>sk</strong>ingu a multi−threadingu je celkom nová trieda problémov, ktoré v jednopouívate¾<strong>sk</strong>ých a jednoúlo−hových prostrediach prakticky neexistovali. Zoberme si ako príklad dva thready, ktoré pra−cujú s rov<strong>na</strong>kými údajmi. V prípade, e kadý thread èíta a zapisuje údaje bez oh¾adu<strong>na</strong> ten druhý, ¾ahko môe dôjs k <strong>na</strong>rušeniu integrity údajov. Majme <strong>na</strong>sledujúci kód:void run(){for (int i = 0; i < 10; i++)x = x + 1;}Nech oba thready vykonávajú tento kód, ktorého úèelom je desakrát inkrementovapremennú x, ktorá je prístupná obom threadom (<strong>na</strong>príklad je statickým èlenom tej trie−dy, do ktorej patrí metóda run()). Predpokladajme, e poèiatoèná hodnota x je rovnánule. Ak <strong>na</strong>jprv spustíme jeden thread a po jeho <strong>sk</strong>onèení druhý, dostaneme výsledok 20.Nechajme teraz oba thready bea <strong>na</strong>raz. V závislosti od okolností bude lea koneènývýsledok niekde v rozsahu od 10 po 20. Ako je to moné?Jadrom problému je riadok x = x + 1 (zámerne nie je pouitý operátor ++). Privyhodnocovaní tohto výrazu thread <strong>na</strong>èíta obsah premennej x do doèasnej pamäte(registra), zvýši ho v nej o jednotku a uloí <strong>na</strong>spä do hlavnej pamäte. Toto vyhodnote−nie však nie je atomické (nedelite¾né) a v prípade, e threadu vyprší èasové kvantum nie−kde „uprostred“, v premennej x zatia¾ zostane stará hodnota. Druhý thread si odteraz spremennou môe robi, èo chce, jeho úpravy budú onedlho zabudnuté; len èo sa toti kslovu opä dostane prvý thread, dokonèí vyhodnotenie výrazu a prepíše momentálnuhodnotu x v hlavnej pamäti hodnotou, ktorá odpoèívala v pomocnom registri.Poznámka: Je pochopite¾né, e pri praktickej realizácii k opísanému problému vôbecnemusí dôjs. Pouitá implementácia JVM môe vyhodnotenie výrazu vyko<strong>na</strong> atomickyalebo budú okolnosti <strong>na</strong>to¾ko priaznivé, e k iadnej chybe nedôjde. To však vo všeobec−nosti oèakáva nemôeme – vdy treba ráta s <strong>na</strong>jhorším moným prípadom.Programátor, ktorý sa spolieha <strong>na</strong> náhodu, si nezaslúi niè iné, len svoje programy za trestpouíva.Ak chce zvedavý èitate¾ vidie efekt vzájomného „lezenia do kapusty“ <strong>na</strong> vlastné oèi,staèí inkriminovaný výraz rozpísa <strong>na</strong>príklad takto:int x_tmp = x;yield();x = x_tmp + 1;Uvedené tri riadky simulujú vyhodnotenie pôvodného výrazu s vynúteným prepláno−vaním <strong>na</strong> druhý thread medzi <strong>na</strong>èítaním starej a uloením novej hodnoty x. Výslednáhodnota x bude pravdepodobne 10.Tento príklad len jemne <strong>na</strong>z<strong>na</strong>èuje celú sféru problémov, ktoré môu vzniknú pri sú−benej èinnosti viacerých threadov a procesov. Riešenie spoèíva v pouití niektorej zosynchronizaèných metód. Rozsah èlánku, bohuia¾, nedovo¾uje zaobera sa týmitometódami podrobnejšie, pretoe ide o ve¾mi rozsiahlu (ale o to zaujímavejšiu) tému.Pozrime sa preto len <strong>na</strong> monosti, ktoré <strong>na</strong> vyriešenie synchronizaèných problémovpo<strong>sk</strong>ytuje Java.PRÍKAZ synchronized. Úsek kódu, ktorý by sa mal vykonáva atomicky, t. j.maximálne jedným threadom súèasne, <strong>na</strong>zveme v súlade s tradíciami kritickou sekciou.Vylúèi súèasnú prítomnos viacerých threadov v kritickej sekcii je jednoduché: nepovolí−me do nej vstup v prípade, e sa tam <strong>na</strong>chádza iný thread. Je, samozrejme, nevyhnutné,aby thready pred vstupom do kritickej sekcie svoj úmysel dali <strong>na</strong>javo.Existuje mnoho spôsobov, ako zabezpeèi toto tzv. vzájomné vyluèovanie (mutual exclu−sion). Java pouíva zámky (locks) a z nich vychádzajúcu implementáciu monitorov (pozriïalej). Zámok je metaobjekt, ktorý nie je priamo prístupný programátorovi a viae sa vdy <strong>na</strong>existujúci objekt (to, mimochodom, z<strong>na</strong>mená, e pomocou primitívnych typov zamykanemono). Pre kadú kritickú sekciu v programe by mal existova samostatný zámok.Pred vstupom do kritickej sekcie sa thread pokúsi zí<strong>sk</strong>a zámok <strong>na</strong>d vybraným objek−tom. Ak sa mu to podarí, zámok sa uzamkne a thread môe vykonáva kód kritickejoblasti. Ak sa mu to nepodarí, pretoe zámok je u zamknutý, z<strong>na</strong>mená to, e v kritickejoblasti je niekto iný. Thread bude preto pozastavený a opä sa rozbehne a po odo−mknutí zámku. Nevstupuje však do kritickej sekcie automaticky, ale opakuje svoj pokus ozí<strong>sk</strong>anie zámku. Medzi okamihom, keï thread prešiel zo spiaceho stavu do stavu pripra−venosti, a okamihom, keï mu bol pridelený procesor, toti mohol do kritickej sekcie vstú−pi ïalší thread (hoci aj ten istý, èo v nej bol predtým). e to nie je spravodlivé? Nu, vparalelnom prostredí si príliš ne<strong>na</strong>vyberáme…V zdrojovom kóde kritickú sekciu musíme „obali“ do príkazu synchronized. Ten má<strong>na</strong>sledujúcu syntax:synchronized ( v raz ){telo (kritická sekcia)}Výsledkom výrazu musí by nenulová referencia <strong>na</strong> existujúci objekt. Pred zaèatímvykonávania tela príkazu sa thread pokúsi zí<strong>sk</strong>a zámok <strong>na</strong>d týmto objektom. Ako zamy−kací objekt pouijeme niektorý z objektov, s ktorými pracujeme v kritickej sekcii, alebosi vypomôeme pomocným objektom:Object lock = new Object();synchronized (lock){...}Sémantika zamykania objektov v Jave dovo¾uje threadu, ktorý vlastní zámok <strong>na</strong>d neja−kým objektom, zí<strong>sk</strong>a tento zámok ešte raz. To je dôleité <strong>na</strong>príklad v takomto prípade:synchronized (lock){synchronized (lock){// ...}}Thread sa nezasekne <strong>na</strong> druhom príkaze synchronized, ale plynule prejde do kritic−kej sekcie.1/2002 PC REVUE 111
P R O G R A M U J E M EDôleitá poznámka: Je nevyhnutné zabezpeèi, aby sa k premenným a objektom, <strong>sk</strong>torými pracujeme v kritickej sekcii, dalo v programe pristupova len cez príkazy synchronized,ktoré <strong>na</strong>vyše musia pracova s rov<strong>na</strong>kým zamykacím objektom. V opaènomprípade synchronizácia stráca zmysel.MONITORY. V paralelnom programovaní pojem monitor opisuje údajovú metaštruktúru,ktorú by sme mohli pripodobni k javov<strong>sk</strong>ej triede: monitor má vnútorný stav (údajové èleny)a po<strong>sk</strong>ytuje operácie, ktoré mono <strong>na</strong>d monitorom vykonáva (metódy). Podstatným rozdie−lom oproti triedam je zabezpeèenie vzájomného vyluèovania pri prístupe k monitoru. I<strong>na</strong>kpovedané: z metód monitora môe by súèasne volaná <strong>na</strong>jviac jed<strong>na</strong>.Implementácia monitorov v Jave je logickým dôsledkom pouitia mechanizmu zám−kov. Ak sa zamyslíme <strong>na</strong>d tým, ako by sa dalo vzájomné vyluèovanie pri prístupe k metó−dam triedy realizova, rýchlo prídeme <strong>na</strong> <strong>na</strong>jjednoduchšiu monos: obali kód metódy dopríkazu synchronized a ako zamykací objekt poui this. Ide to však ešte jedno−duchšie. Ak pri deklarácii metódy pouijeme ako jeden z modifikátorov k¾úèové slovosynchronized, dosiahneme tým, e pri volaní metódy sa aktuálny thread <strong>na</strong>jprv pokú−si zí<strong>sk</strong>a zámok <strong>na</strong>d objektom, <strong>na</strong>d ktorým metódu volá. V prípade, e ide o statickúmetódu, v rámci ktorej objekt this neexistuje, ako zamykací objekt sa pouije objekttypu Class reprezentujúci danú triedu.Ako jednoduchý príklad monitora si ukáme triedu, ktorá vyrieši náš predchádzajúciproblém s paralelnou inkrementáciou premennej x:class MutExVar{private int value;public MutExVar(int _val){ value = _val; }public synchronized void set(int _val){ value = _val; }public synchronized int get(){ return value; }public synchronized void inc(){ value ++; }}Oba thready budú <strong>na</strong>miesto príkazu x = x + 1 v cykle vola metódu x.inc() (pred−pokladáme, e x je teraz inštanciou triedy MutExVar). Ïalšie synchronizované metódyset() a get() slúia <strong>na</strong> <strong>na</strong>stavenie a zí<strong>sk</strong>anie uloenej hodnoty. Konštruktor z pocho−pite¾ných dôvodov nemusí by synchronizovaný.PROBLÉM S DEADLOCKOM. Pri práci so zámkami sa treba vyvarova situá−cie, ktorá sa bene oz<strong>na</strong>èuje anglickým termínom deadlock, po sloven<strong>sk</strong>y uviaznutie.Okolo problému deadlocku existuje celá teória, take len struène: stav deadlocku z<strong>na</strong>me−ná, e jeden alebo viacero threadov neobmedzene dlho èaká <strong>na</strong> vstup do kritickej sekcie(a nikdy sa do nej nedostane).Ako môe k deadlocku dôjs? Predstavme si dva thready, ktoré <strong>na</strong> vstup do kritickejsekcie potrebujú zí<strong>sk</strong>a dva zámky, ale budú to robi v <strong>na</strong>vzájom opaènom poradí. Prvýthread <strong>na</strong>príklad takto:synchronized (lock1){synchronized (lock2){// ...}}a druhý takto:synchronized (lock2){synchronized (lock1){// ...}}Ak sa prvému threadu podarí zí<strong>sk</strong>a lock1 a druhému lock2, v tom okamihu súodsúdení <strong>na</strong> veèné èakanie, pretoe ani jeden nemôe zí<strong>sk</strong>a druhý zámok (má ho tendruhý). Vyriešenie tohto konkrétneho problému je jednoduché – staèí zameni poradiepríkazov synchronized v niektorom z threadov. Univerzálny liek však <strong>na</strong> odstránenienebezpeèenstva deadlocku neexistuje a pri písaní programov treba pouíva predovšet−kým zdravý rozum a riadnu dávku predstavivosti.PODMIENEÈNÉ PREMENNÉ. Java po<strong>sk</strong>ytuje ešte jeden synchronizaèný pro−striedok, ktorý sa <strong>na</strong>jviac podobá konceptu podmieneèných premenných (conditio<strong>na</strong>lvariables). Tie si môeme predstavi ako metaobjekty, ktoré predstavujú synchronizaèné bodyviazané <strong>na</strong> splnenie nejakej podmienky. Po<strong>sk</strong>ytujú dve operácie s tradiènými názvami waita sig<strong>na</strong>l, ktorým v Jave zodpovedajú metódy wait() a notify(). Ide o metódy triedyObject, a teda ich môeme zavola <strong>na</strong>d kadou inštanciou objektového typu.Aká je sémantika týchto dvoch operácií? Ak thread zistí, e podmienka, ktorá je pridruenák podmieneènej premennej, nie je splnená, vykoná operáciu wait. Tým sa zaradí do zoz<strong>na</strong>muèakate¾ov <strong>na</strong> danú podmieneènú premennú, prejde do stavu „spiaci“ a <strong>na</strong>ïalej nesúaí opridelenie procesora. Zo spánku ho môe zobudi iný thread, ktorý <strong>na</strong>d podmieneènou pre−mennou vykoná operáciu sig<strong>na</strong>l. Dôsledkom tejto operácie je výber jedného z èakate¾ov ajeho zobudenie. Zobudený thread sa presúva do stavu „pripravený“ a èaká <strong>na</strong> pridelenie pro−cesora. Ak je zoz<strong>na</strong>m èakate¾ov prázdny, operácia sig<strong>na</strong>l nemá nijaký efekt.Pozrime sa teraz <strong>na</strong> vec <strong>na</strong> úrovni zdrojového kódu. Operácii wait zodpovedá zavola−nie metódy wait() <strong>na</strong>d ¾ubovo¾ným objektom (ktorý <strong>na</strong>hrádza spomí<strong>na</strong>nú podmieneè−nú premennú). Aktuálny thread musí <strong>na</strong>d týmto objektom vlastni zámok. V rámci vyko−návania metódy je thread pozastavený a zámok sa mu odoberie (to je nevyhnutný krok,pretoe i<strong>na</strong>k by spiaci thread blokoval akýko¾vek prístup k synchronizaènému objektu).Pri volaní metódy wait() mono ako volite¾ný argument zada maximálny èas, poèa<strong>sk</strong>torého thread bude èaka <strong>na</strong> zobudenie.Druhej operácii sig<strong>na</strong>l zodpovedá metóda notify(), ktorú, pochopite¾ne, voláme <strong>na</strong>drov<strong>na</strong>kým objektom ako metódu wait(). Thread, ktorý metódu volá, tak obyèajne èinípreto, lebo zabezpeèil splnenie podmienky pridruenej k objektu. Ako dôsledok volaniametódy notify() sa zobudí jeden z èakajúcich threadov (ktorý to bude, je vecou imple−mentácie). Ak nikto neèaká, metóda je bez efektu. Niekedy príde vhod zobudi všetky èakajú−ce thready – vtedy pouijeme metódu notifyAll(), ktorá sa i<strong>na</strong>k správa ako metódanotify(). Metódy notify() a notifyAll() by mal vola len thread, ktorý je momen−tálne vlastníkom zámku <strong>na</strong>d synchronizaèným objektom.Zobudený thread nezaène bea okamite. Je zaradený do radu threadov èakajúcich<strong>na</strong> <strong>na</strong>plánovanie a po pridelení procesora sa <strong>na</strong>jprv s<strong>na</strong>í zí<strong>sk</strong>a zámok, ktorý mu bolpred pozastavením odobraný. Po zí<strong>sk</strong>aní zámku ukonèí volanie metódy wait() apokraèuje <strong>na</strong>sledujúcim príkazom. V prípade, e thread volal metódu wait() ako dôsle−dok nesplnenia nejakej podmienky, je ve¾mi dôleité test podmienky zopakova, pretoenie je zaruèené, e sa od okamihu zobudenia threadu po jeho opätovné <strong>na</strong>plánovanietáto podmienka nezmenila. Namiesto kódu:synchronized ( obj ){if (! podmienka )obj.wait();}treba bezpodmieneène poui kód:synchronized ( obj ){while (! podmienka )obj.wait();}A ešte jed<strong>na</strong> poznámka <strong>na</strong> záver: Threadu sa poèas vykonávania metódy wait() odo−berá len zámok <strong>na</strong> aktuálny objekt. Prípadné ïalšie zámky mu zostávajú a sú tak dobrýmkandidátom <strong>na</strong> vznik deadlocku.PRÍKLADY. Zdrojové texty k príkladom mono tradiène nájs <strong>na</strong> webovej stránke PCREVUE. Nachádza sa tam okrem iného riešenie klasického problému synchronizácie prístupuku koneènému bufferu (i<strong>na</strong>k známeho aj ako problém producentov/konzumentov).Vladimír Klimov<strong>sk</strong>ý112 PC REVUE 1/2002