NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!
NOVINKA - Online rekvalifikační kurz Java programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.

Lekce 2 - Multithreading v Javě - Daemon, join a synchronized

V minulé lekci, Multithreading 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 IllegalThreadStateException.

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:

Konzolová aplikace
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:

Konzolová aplikace
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ím uvedením 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, v lekci Multithreading v Javě - Synchronizace v praxi :)


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

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

 

Předchozí článek
Multithreading v Javě
Všechny články v sekci
Vícevláknové aplikace v Javě
Přeskočit článek
(nedoporučujeme)
Multithreading v Javě - Synchronizace v praxi
Článek pro vás napsal Matěj Kripner
Avatar
Uživatelské hodnocení:
33 hlasů
Student, programátor v CZ.NIC.
Aktivity