LINUXSOFT.cz Přeskoč levou lištu
Uživatel: Heslo:  
   CZUKPL

> Java (20) - vlákna

Vlákna (threads) jsou v Javě velmi důležitá. Je to prakticky jediný způsob, jak používat mnohé blokující operace. Využití mají vlákna také pro činnosti prováděné na pozadí.

2.11.2005 07:00 | Lukáš Jelínek | Články autora | přečteno 48482×

Úvod do vláken v Javě

Vlákna představují způsob, jak v rámci jednoho procesu provádět více činností paralelně (nebo - u jednoprocesorového stroje - pseudoparalelně). V rámci každého z vláken je vykonáván kód nezávisle na ostatních vláknech. Od procesů se vlákna liší tím, že spolu sdílejí data procesu, v němž běží.

Standardní knihovny Javy obsahují poměrně rozsáhlou podporu pro práci s vlákny. Než je ale člověk začne používat (což je samo o sobě velice jednoduché), je dobré znát pár věcí, které jsou s používáním vláken v Javě nerozlučně spjaty.

Kvůli platformové nezávislosti Javy není přesně definováno, jak se vlákna budou konkrétně chovat. Existují ale v zásadě dva druhy vláken:

  • native threads - vlákna poskytovaná operačním systémem. V Linuxu dříve výhradně LinuxThreads (klasické procesy se sdílenými daty), od JDK 1.4.2 lze využít i Native Posix Threads, což poskytuje podstatně vyšší výkon hlavně v těch případech, kdy se často vytvářejí nová vlákna.
  • green threads - vlákna zajištěná na úrovni virtuálního stroje Javy. Běžně se s nimi nesetkáváme, ale pro dobrou přenositelnost je dobré s nimi počítat.

Native threads

Nativní vlákna mají takové vlastnosti, jaké jim poskytuje jejich implementace v OS. Typicky lze využít ryze paralelní běh (na více procesorech současně) a preemptivní přidělování a odnímání procesoru jednotlivým vláknům. O tato vlákna se programátor prakticky nemusí starat, protože starosti leží na operačním systému.

Green threads

"Zelená" vlákna běží jen na jediném procesoru (jde tedy o pseudoparalelní běh) a tento procesor nelze vláknu zvenku odebrat. Proto se programátor musí postarat, aby žádné vlákno neběželo příliš dlouho (aby se dostalo na ta ostatní). Lze to realizovat různými cestami, brzy se k tomu dostaneme.

Je dobrým zvykem navrhovat programy tak, aby vyhověly oběma modelům - a to přesto, že se s green threads už příliš nepočítá.

Rozhraní Runnable a třída Thread

Naprostým základem práce s vlákny v Javě je rozhraní Runnable. Je v balíku java.lang a obsahuje jedinou metodu run(). Právě tato metoda obsahuje kód, který se bude v rámci vlákna vykonávat. Dále tu máme třídu Thread, která toto rozhraní implementuje, a hlavně, obsahuje infrastrukturu pro řízení běhu vlákna.

Potřebujeme-li použít vlákno, lze postupovat dvěma cestami:

  • rozšíření třídy Thread (s nezbytným předefinováním metody run())
  • implementace rozhraní Runnable

Z hlediska výkonného kódu je v podstatě jedno, kterou cestu použijeme - na implementaci metody run() se to většinou neprojeví. Liší se jen přípravné práce. V zásadě se dá říct, že v jednodušších případech je lepší rozšířit třídu Thread, a v těch složitějších použít druhý způsob.

Vytvoření a spuštění vlákna

Vlákna s bohatou činností vytváříme jako klasické pojmenované třídy, pro jednoduché operace si vystačíme s anonymními třídami. Nyní se podívejme na dva příklady. První z nich ukazuje, jak vytvořit vlákno na základě třídy Thread:

Thread t = new Thread() {
    public void run() {
        // tady bude nějaký kód
    }
};

t.start();

Takto můžeme v okamžiku potřeby snadno vytvořit vlákno v místě, kde ho použijeme. Vlákno se spustí zavoláním metody start(). Druhý příklad ukáže, jak vytvořit vlákno na základě rozhraní Runnable. Přestože by šlo použít anonymní třídu, zde bude použita třída pojmenovaná:

class ThreadUser {

    static class MyRunnable implements Runnable {
        public void run() {
            // tady bude nějaký kód
        }
    }

    public static void main(String args[]) {
        Thread t = new Thread(new MyRunnable());
        t.start();
        ...
    }   
}

Výsledek bude tentýž jako v předchozím případě. Rozdíl je v tom, že zde vytvoříme instanci (nijak neupravené) třídy Thread samostatně a předáme jí instanci třídy implementující rozhraní Runnable. Když se pak instanci Thread zavolá metoda start(), způsobí to, že se v rámci vlákna začne vykonávat metoda run() v "asociovaném" objektu MyThread (implementující Runnable).

Životní fáze vlákna

Vlákno od svého vytvoření (myšleno konstruktorem třídy Thread) do finalizace prochází řadou fází - do některých z nich se může, ale také nemusí dostat. Jedná se o tyto fáze:

  1. Vytvořené, nespuštěné - vlákno bylo vytvořeno jako instance objektu, a dosud nebylo spuštěno (nevykonává se kód). Některá nastavení vlákna lze provést jen v tomto stavu.
  2. Spuštěné, běžící - vlákno běží, procesor vykonává jeho kód.
  3. Spuštěné, čekající - vlákno může běžet, ale čeká na přidělení procesoru.
  4. Spuštěné, uspané nebo zablokované - vlákno bylo uspáno metodou sleep() nebo zablokováno voláním wait(), join() či jiným způsobem (např. v blokující operaci).
  5. Ukončené - vlákno doběhlo (vyskočilo z metody run()) a pouze přečkává, než bude (po ztrátě referencí) odstraněno jako instance kteréhokoli objektu.

Uvedené stavy jsou chápány z hlediska logického, nikoli implementačního (vnitřně se např. rozlišuje stav blokovaného vlákna a vlákna čekajícího na nějakou událost). Existují ještě další fáze, ale do nich se vlákno může dostat pouze zavoláním některé ze zavržených (deprecated) metod. Proto je lepší se o těchto stavech vůbec nezmiňovat, případné zájemce odkazuji na dokumentaci.

Operace s vláknem

Normální vlákno, jak jsme ho vytvořili v příkladech, běží vedle hlavního vlákna (toho, které běží od začátku programu), a program skončí až v momentě, kdy dokončí běh všechna taková vlákna. Někdy je ale třeba, aby běh programu závisel pouze na jediném vlákně nebo omezené skupině, a zbylá vlákna na to vliv neměla. K tomu slouží tzv. démoni. Démona vytvoříme z normálního vlákna (resp. to jde i obráceně) metodou setDaemon(). Musí se to ale udělat před spuštěním vlákna, jinak se dočkáme výjimky IllegalThreadStateException.

Vláknům můžeme nastavovat priority. Nově vytvořené má výchozí prioritu (střední), můžeme nastavit větší nebo menší hodnotu. Interpretace záleží na konkrétní implementaci vláken. U native threads je převedena na prioritu vlákna v operačním systému, kdežto u green threads má vlákno s vyšší prioritou vždy absolutní přednost před vláknem s prioritou nižší (pozor na to!). Priorita se nastavuje voláním setPriority() v rozsahu od MIN_PRIORITY do MAX_PRIORITY; lze to provést před spuštěním vlákna i za běhu.

Pro snazší práci (hlavně při ladění) si lze vlákna pojmenovávat. Jméno se určí buď v konstruktoru, nebo později metodou setName(). Jména vláken nemusí být unikátní.

Nesobecká vlákna

Jak jsem se již zmínil, správně napsaný multithreadový program by neměl spoléhat na konkrétní implementaci vláken. S tím souvisí důležitá podmínka, aby vlákna tzv. "nebyla sobecká" - jinými slovy, aby si neusurpovala procesor tak, že tím ostatním vláknům brání v běhu.

Je proto nutné zajistit, aby se každé vlákno dostatečně často vzdávalo procesoru. K tomu dochází v těchto případech:

  • blokující I/O operace (např. read(), write(), accept()), synchronizační operace (wait(), join())
  • uspání vlákna voláním sleep()
  • explicitním vzdáním se procesoru - metoda yield()
  • volání některých zapovězených metod (nebudu uvádět)

Uspání vlákna je spolehlivé, ale ne vždy ho potřebujeme. Spoléhání na blokující operace je ošemetné, protože často k zablokování dojít nemusí a vlákno poběží dál. Naproti tomu zavolání yield() vynutí nové naplánování vlákna, a proto funguje zcela spolehlivě (pozor ale na priority!).

Uvedený způsob má ale jednu nevýhodu - vhodně umístit volání yield() totiž v řadě případů vůbec není triviální, a špatné rozmístění může mít podobný efekt, jako kdyby se to neudělalo vůbec. Proto existuje ještě jedna cesta - vytvoření speciální "plánovacího" vlákna. Toto vlákno bude mít maximální prioritu, většinu času bude uspáno, jen občas se probudí a zase hned usne. Tím dojde ale k naplánování jiného ze zbývajících vláken, takže to má ve výsledku podobný efekt, jako kdyby se vlákna plánovala nativně. Důležité je ale zvolit vhodnou granularitu (délku maximálního časového kvanta) - pro většinu případů lze použít hodnoty 5-50 ms.

Synchronizace přístupu

Multithreading přináší mnoho výhod, ale také určité nevýhody. Jednou z nich je nutnost synchronizovat přístup k datům tak, aby byla zaručena jejich integrita a konzistence. Obecně je lepší synchronizovat spíš více než méně, protože nadbytečná sychronizace pouze zpomaluje, kdežto nedostatečná vážně narušuje funkci programu. Vždy je ovšem potřeba dát si pozor, aby se vlákna nemohla vzájemně zablokovat (deadlock). Proto je nutné snažit se (již ve fázi návrhu), aby synchronizovaných míst bylo co nejméně.

Pro synchronizaci máme opět více možností:

synchronized metoda

Metoda může být deklarována s modifikátorem synchonized. To znamená, že v okamžiku vstupu do metody se objekt zamkne a při opuštění odemkne. Zavolá-li metodu jiné vlákno, musí čekat, než ji opustí vlákno, které ji zavolalo dřív.

class MyClass {
    private int x = 0;
    private int y = 0;
    
    public synchronized void setData(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Metoda setData() v příkladu pracuje tak, že je během modifikace dat vyloučen přístup z jiného vlákna. Je zde totiž žádoucí, aby se proměnné x a y měnily vždy současně, proto nelze připustit, aby si někdo přečetl jejich hodnoty v okamžiku, kdy je jedna změněna a druhá nikoli.

synchronized blok

Předchozí řešení je velice jednoduché a elegantní, ale jednak může zamykat na zbytečně dlouhou dobu (což by se muselo řešit rozdělením na více metod), a za druhé vyžaduje, aby byla příslušná třída již takto implementována. Máme-li třídu, která (typicky z výkonnostních důvodů) nezamyká objekt při jeho modifikaci, nejjednodušší řešení je použít blok s deklarací synchronized a uvedením objektu, který se má zamknout.

java.awt.Point p = new java.awt.Point(10, 20);

...

synchronized (p) {
    p.setLocation(5, 50);
}

Metoda setLocation() není synchronizovaná, proto je nutno (při přístupu z více vláken) synchronizovat zvenku. Funkce uvedeného kódu je zřejmá.

Poznámka: S třídou Point se běžně nepracuje tak, aby k ní mohlo přistupovat více vláken (proto je z výkonnostních důvodů bez synchronizace). Proč tomu tak je, si řekneme později, v úvodu do javovské grafiky.

Synchronizační wrappery

V kapitole o kolekcích jsme se setkali s tzv. synchronizačními wrappery. Použijí se v případě, kdy máme kolekce bez synchronizace přístupu. Wrapper navenek zapouzdří příslušný objekt, aniž by se změnilo jeho rozhraní, a o synchronizaci se postará.

Čekání na konec jiného vlákna

Zavoláme-li nějakému jinému vláknu metodu join(), aktuální vlákno se zastaví a bude čekat na skončení běhu onoho vlákna. Lze využít i verze s časovým limitem - pak se bude čekat maximálně po zvolenou dobu.

Čekání na objektu

Každý objekt (jakýkoli potomek třídy Object) má sadu metod wait(). Zavolání této metody způsobí, že se aktuální vlákno zastaví do doby, než bude uvolněno zavoláním metody notify() nebo notifyAll() tomuto objektu. První metoda uvolní právě jedno vlákno, druhá všechna vlákna. Vlákno, které bude některou z těchto metod volat, si objekt musí nejprve zamknout - buď v rámci synchronized metody, nebo v synchronized bloku. Čekání lze opět omezit časovým limitem.

Modifikátor volatile

Pracuje-li s jednou proměnnou více vláken, obecně není zaručeno, že každé z vláken uvidí správnou hodnotu, přestože je modifikační operace atomická. Je to proto, že se přístup k datům optimalizuje a vlákna používají lokální kopie, jejichž obsah se nemusí včas promítnout do původní proměnné. Pokud se proměnná deklaruje s modifikátorem volatile, každá změna je okamžitě viditelná pro všechna vlákna a další synchronizace již není nutná.

Přerušení vlákna

Přerušení je událost, kterou je nutno zvlášť ošetřit. Lze ji přirovnat k příchodu signálu do procesu. Pokud vlákno běží, je pouze nastaven příznak, že došlo k přerušení (lze zjistit zavoláním isInterrupted() nebo interrupted(); pozor - metoda interrupted() příznak resetuje!). Vlákno se přerušuje voláním interrupt().

Pokud vlákno čekalo (uspáno nebo v blokující operaci), je navíc vyvolána výjimka InterruptedException a vlákno se rozběhne. Tato výjimka je synchronní, její ošetření je tedy u daných operací povinné. Přerušit čekající vlákno lze např. v okamžiku, kdy se má ukončit program a vlákno by po sobě mělo uklidit. V minulé kapitole bychom takto třeba uzavřeli otevřený socket:

try {
    ServerSocket ss = new ServerSocket(22222);
    
    try {
        while (!quit) {
            final Socket sock = ss.accept();
            Thread t = new Thread() {
                public void run() {
                    try {
                        InputStream is = sock.getInputStream();
                        OutputStream os = sock.getOutputStream();
                         
                        ...
                        
                        sock.close();
                    } catch (IOException e) {
                        ...
                    }
                }
            };
            t.setDaemon(true);
            t.start();
        }
    } catch (InterruptedException e) {  // zde se zachytí přerušení
        ss.close();                     // uzavření socketu
    }
} catch (Exception e) {
    ...
}

Skupiny vláken

V rozsáhlejších programech často pracujeme s mnoha vlákny, která se dají rozdělit do různých logických skupin. V jedné mohou být třeba vlákna obsluhující síťové požadavky, ve druhé vlákna pro zpracování dat atd. Jejich správu si můžeme usnadnit využitím třídy ThreadGroup.

Skupiny, tvořené instancemi ThreadGroup, jsou hierarchicky (stromově) organizovány. Lze tak vlákna ovládat na různých úrovních podle toho, jak právě potřebujeme. Každá skupina může být, podobně jako samotné vlákno, libovolně pojmenována.

V rámci skupin lze např. určovat vláknům maximální prioritu nebo vlákna hromadně přerušovat. Do skupiny lze vlákno přidat jen v okamžiku jeho vytváření (skupina se předá jako parametr konstruktoru), později již změna není možná.

ThreadGroup tg = new ThreadGroup("network server threads");
tg.setDaemon(true);

Runnable r = new Runnable() {
    public void run() {
        ...
    }
};

Thread t1 = new Thread(tg, r);
Thread t2 = new Thread(tg, r);

V příkladě se vytvoří skupina vláken - tako skupina bude démon, tzn. bude automaticky zrušena v okamžiku, kdy doběhne poslední vlákno. Při vytváření vláken jim (kromě instance implementující Runnable) předáme i tuto skupinu, čímž se vlákna stanou jejími členy.

Lokální data vláken

Někdy je vhodné, aby každé vlákno pracoval se specifickými daty, a přesto naprosto stejným způsobem jako jiná vlákna. K tomu slouží třída ThreadLocal, která slouží jako "mikrokontejner" (pojímá jednu hodnotu) pro tato data. Instanci tohoto objektu může každé vlákno nastavit nějakou hodnotu - a je zaručeno, že při požadavku na hodnotu vlákno obdrží vždy právě tu svoji. Pokud si vlákno nic nenastaví, dostane hodnotu null, ledaže by byla (v potomkovi ThreadLocal) předefinována metoda initialValue().

Před JDK 1.5 bylo nutné hlídat si typ dat, a podle potřeby přetypovávat. Od JDK 1.5 (Java 5.0) má ThreadLocal generický charakter, a lze proto provádět automaticky typovou kontrolu.

Další pohled pod kapotu

Jsme na konci poměrně dlouhé kapitoly o vláknech. Přichází vhodná chvíle k dalšímu pohledu pod kapotu - tentokrát na datové typy. Podíváme se na přetypovávání, kontrole typů, zjišťování informací o typech atd. Právě tato část Javy patří k těm nejvíce propracovaným, podle mého názoru je to oblast velice zajímavá.

Verze pro tisk

pridej.cz

 

DISKUZE

mě to hází chybu 2.11.2005 10:52 Jirka Hefrt
  |- Re: mě to hází chybu 2.11.2005 13:34 Karel Honzl
  L Re: mě to hází chybu 2.11.2005 17:36 Robert B
    L Re: mě to hází chybu 3.11.2005 09:34 Lukáš Jelínek
      L Re: mě to hází chybu 6.11.2005 15:04 Lukáš Jelínek




Příspívat do diskuze mohou pouze registrovaní uživatelé.
> Vyhledávání software
> Vyhledávání článků

1.12.2016 22:13 /František Kučera
Máš rád svobodný software a hardware nebo se o nich chceš něco dozvědět? Přijď na sraz spolku OpenAlt, který se bude konat ve čtvrtek 8. prosince od 18:00 v Radegastovně Perón (Stroupežnického 20, Praha 5). Sraz bude tentokrát tématický. Bude retro! K vidění budou přístroje jako Psion 5mx nebo Palm Z22. Ze svobodného hardwaru pak Openmoko nebo čtečka WikiReader. Přijďte se i vy pochlubit svými legendami, nebo alespoň na pivo. Moderní hardware má vstup samozřejmě také povolen.
Přidat komentář

4.9.2016 20:13 /Pavel `Goldenfish' Kysilka
PR: Dne 22.9.2016 proběhne v Praze konference Cloud computing v praxi. Tématy bude např. nejnovější trendy v oblasti cloudu a cloudových řešení, provozování ERP v cloudu, o hostování různých typů softwaru, ale třeba i o zálohování dat nabízeném podnikům formou služby.
Přidat komentář

1.9.2016 11:27 /Honza Javorek
Česká konference o Pythonu, PyCon CZ, stále hledá přednášející skrz dobrovolné přihlášky. Máte-li zajímavé téma, neváhejte a zkuste jej přihlásit, uzávěrka je již 12. září. Konference letos přijímá i přednášky v češtině a nabízí pomoc s přípravou začínajícím speakerům. Řečníci mají navíc vstup zadarmo! Více na webu.
Přidat komentář

27.8.2016 8:55 /Delujek
Dnes po 4 letech komunitního vývoje vyšla diaspora 0.6.0.0
diaspora* je open-source, distribuovaná sociální síť s důrazem na soukromý
Více v oficiálním blog-postu
Přidat komentář

24.8.2016 6:44 /Ondřej Čečák
Poslední týden CFP LinuxDays 2016; pokud byste rádi přednášeli na LinuxDays 2016 8. a 9. října v Praze, můžete svůj příspěvek přihlásit, následovat bude veřejné hlasování.
Přidat komentář

9.8.2016 22:56 /Petr Ježek
Zařazení souborového systému reiser4 do jádra 4.7 znamená konečně konec patchování jádra jen kvůli možnosti použít reiser4.
Přidat komentář

12.7.2016 13:14 /František Kučera
Spolek OpenAlt zve na 130. distribuovaný sraz příznivců svobodného softwaru a otevřených technologií (hardware, 3D tisk, SDR, DIY, makers…), který se bude konat ve čtvrtek 21. července od 18:00 v Radegastovně Perón (Stroupežnického 20, Praha 5).
Přidat komentář

11.7.2016 16:53 /Redakce Linuxsoft.cz
Konference LinuxDays hledá přednášející. Přihlášky poběží do konce prázdnin, v září bude hlasování a program. Více na https://www.linuxdays.cz/2016/cfp/.
Přidat komentář

   Více ...   Přidat zprávičku

> Poslední diskuze

9.11.2016 7:42 / Mane
hardwood floor waxing

8.11.2016 13:38 / Mira
Konfigurace maldet na Centos serveru

2.11.2016 11:06 / Warlock
Odkaz v PHP

20.10.2016 0:13 / Jan Kuba
Re: Basic

19.9.2016 21:04 / Marek Schoř
Poděkování

Více ...

ISSN 1801-3805 | Provozovatel: Pavel Kysilka, IČ: 72868490 (2003-2016) | mail at linuxsoft dot cz | Design: www.megadesign.cz | Textová verze