Přidej si svou IT školu do profilu a najdi spolužáky zde na síti :)

2. díl - Multithreading v Javě - Daemon, join a synchronized

Java Vlákna Multithreading v Javě - Daemon, join a synchronized

ONEbit hosting Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulém dílu o multithreadingu 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 IllegalThread­StateException.

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:

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 :D 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:

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:

  1. 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.
  2. Vytvořením vlastního bloku synchronized a externí uvedení 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 :)


 

Stáhnout

Staženo 91x (2.27 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

 

Článek pro vás napsal Matěj Kripner
Avatar
Jak se ti líbí článek?
8 hlasů
Věnuji se programování a dalším věcem, které považuji za důležité pro maximalizaci doby přežití naší civilizace.
Miniatura
Předchozí článek
Multithreading v Javě
Miniatura
Všechny články v sekci
Vícevláknové aplikace v Javě
Aktivity (3)

 

 

Komentáře

Avatar
mara
Člen
Avatar
mara:11.11.2014 14:57

Nadherny článek :) jen tak dál :)

Odpovědět 11.11.2014 14:57
Co na srdci, to na Facebooku
Avatar
B42P6
Redaktor
Avatar
B42P6:4.7.2015 19:34

Fajn článok, inak načo je v Hlavičke metody main aj

throws InterruptedException

keď máš výnimku "odchytenú" v catch bloku.
Nabudúce môžeš na vypisovanie znakov v textovom reťazci použiť foreach miesto while.

Editováno 4.7.2015 19:38
Odpovědět 4.7.2015 19:34
'long long long' is too long for GCC
Avatar
Matěj Kripner
Redaktor
Avatar
Odpovídá na B42P6
Matěj Kripner:5.7.2015 12:37

Díky, po nějaké době procházím ten kód znovu a není úplně ideální. Opravím to hned, jak se dostanu k PC.

Odpovědět 5.7.2015 12:37
To understand what recursion is, you must first understand recursion.
Avatar
B42P6
Redaktor
Avatar
B42P6:5.7.2015 15:53

Ok, dík za "odozvu". :D

Odpovědět 5.7.2015 15:53
'long long long' is too long for GCC
Avatar
nehila.peter
Člen
Avatar
Odpovídá na B42P6
nehila.peter:1.2.2016 19:42

Dalej v kóde už catch nemá , preto je tam ta čast

throws InterruptedException
 
Odpovědět 1.2.2016 19:42
Avatar
Zdeněk Zemek:6. února 10:25

Super článek, moc děkuji....

 
Odpovědět 6. února 10:25
Avatar
Lubor Pešek
Člen
Avatar
Lubor Pešek:29. srpna 13:39

Chtěl bych se zeptat....
Dá se nějak buď demonizovat nebo jakkoliv ovládat to hlavní vlákno? Mám dva objekty v samostatných vláknech a díky běhu hlavního vlákna se mi nedaří démon jednoho z těch dvou objektů.

Odpovědět 29. srpna 13:39
Existují dva způsoby, jak vyřešit problém. Za prvé vyhoďte počítač z okna. Za druhé vyhoďte okna z počítače.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 7 zpráv z 7.