IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

2. diel - Multithreading v Jave - Daemon, join a synchronized

V minulej lekcii, Multithreading v Jave , sme si urobili stručný úvod do vláknového modelu Javy a osvojili sme si základy práce s vláknami. Bavili sme sa aj o hlavnom vláknu. Možno ale nebolo úplne jasné, prečo je vlastne hlavný vlákno hlavným.

Toto vlákno, ktoré býva označované za hlavné, je dôležité najmä preto, že sa automaticky spustí v okamihu, keď je spustený javovský program. Do tej doby, než sú z neho vytvorená a spustená ďalšie vlákna, je vlastne hlavný vlákno synonymum pre samotný program. Hlavné vlákno ale nemusí byť (hoci je to zvykom) ukončené ako posledný.

Zoberme si triedu Vlakno a metódu main() z minulého dielu:

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");
}

Nahraďme v metóde main príkaz Thread.sleep(750) za Thread.sleep(200). Keď teraz program spustíme, zistíme, že druhé vlákno pokračovalo aj po ukončení hlavného vlákna. Všeobecne platí, že program prebieha, kým je vykonávané aspoň jedno vlákno, ktoré nie je označené ako daemon. A to bez ohľadu na to, či je vlákno hlavné, alebo nie. To znamená, že počet bežiacich daemon vlákien nemá žiadny vplyv na ukončenie programu.

Daemon vlákna beží na pozadí programu podobne ako Garbage collection. Beh týchto vlákien má zmysel iba za prítomnosti ďalších vlákien - práve preto dochádza k ich automatickému ukončeniu. Dobrým príkladom môže byť napr. Nejaký timer. Non-daemon vlákna sú niekedy označovaná ako user threads.

Pri vytvorení má každé vlákno hodnotu atribútu daemon nastavenú na false. To môžeme zmeniť inštančný metódou setDaemon(boolean daemon). Ale pozor, túto metódu možno volať len pred spustením vlákna. Ak je pravidlo porušené, je vyvolaná výnimka IllegalThreadStateException.

Umiestni teda pred volanie metódy start() na inštanciu mojeVlakno v metóde main() tento kód:

mojeVlakno.setDaemon(true);

Ak teraz spustíme program, vykoná sa len časť behu vedľajšieho vlákna a správa o jeho ukončení sa nikdy nezobrazí. Je to samozrejme preto, že vlákno je označené ako daemon a vo chvíli, kedy sa skončí beh hlavného vlákna, program skončí.

Komunikácia vlákien

Až do teraz sme si situáciu s plánovaním behu vlákien veľmi zjednodušovali používaním metódy Thread.sleep(). Keď vlákno vykonáva len triviálne operácie a potom dlhú dobu čaká, možno celkom dobre predpovedať, ako bude beh vlákna prebiehať. V reálnych aplikáciách ale budú vlákna vykonávať rôzne výpočty alebo čakať na vstupy a my nebudeme môcť presne povedať, ako dlho bude danému vláknu trvať jeho beh.

Našťastie existuje celá rada dômyselných metód, ktoré nám dovoľujú vykonávať akúsi mezivláknovou komunikáciu. Jedná sa o pokročilejšie tému späté so synchronizáciou, takže jeho miesto bude v našom seriáli až ďalej. Už teraz si však ukážeme dve, veľmi praktické metódy - join() a isAlive().

IsAlive ()

Metóda isAlive() je inštančný metóda triedy Thread vracajúci true, ak je vlákno voči ktorému bola volaná stále bežiaci. V opačnom prípade vracia false. Skúsme navrhnúť program tak, aby hlavné vlákno s využitím metódy isAlive() "počkalo" na vlákno vedľajšej:

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");
}

Trieda Vlakno zostáva bezo zmeny. Tu asi nie je čo vysvetľovať. Mal by sa zobraziť nasledujúci výstup:

Konzolová aplikácia
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

Nie je to zlé, program funguje ako má. My sa ale s týmto riešením neuspokojíme : D Nebola by to totiž Java, keby nám neponúkala lepšie riešenie. Tým je druhá zo spomínaných metód - metóda join().

Join ()

Metódu join() tiež obsahujú inštancie triedy Thread, jej použitie je ale komplexnejšie. Zaisťuje totiž čakanie vlákna, z ktorého bola volaná na vlákno voči ktorému bola volaná. Predchádzajúci kód metódy main() by sa tak dal zredukovať 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 spustení sa zobrazí identický výstup.

Synchronizácia

Obsiahlym témou multithreadingu je synchronizácia. Je to vlastne spôsob zaisťujúce, že v jednom okamihu bude mať k danému prostriedku prístup iba jedno vlákno. Predstavme si situáciu, kedy viac vlákien pristupuje k nejakej zložitej štruktúre - napr. Kolekciu. V takom prípade musí existovať spôsob, ako zabrániť tomu, aby si vlákna vzájomne "liezla do práce". Dajme tomu, že jedno vlákno bude prechádzať jeden prvok kolekcie po druhom a vypisovať ich. Zároveň ale druhé vlákno bude vyrábať a vkladať do kolekcie ďalšie prvky. Čo by sa presne stalo záleží na konkrétnom type kolekcie, isté ale je, že postup by neviedol k očakávanému výsledku. Ba čo horšie, výsledok by ani nebol rovnaký pre všetkých spustenie, takže by ho nebolo možné predvídať.

Skúsme si vytvoriť podobný príklad. Niekoľko vlákien bude súčasne po častiach vypisovať nejaký text. Pozmení teda 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 príklade vypisuje súčasne niekoľko vlákien zadaný text tak, že odosiela do konzoly jeden znak za druhým. V cykle metódy run navyše voláme Thread.sleep(1) - tým simulujeme vykonávanie časovo náročnejšie operácie ako je výpis znaku. My už tušíme, že výstup bude zakaždým iný a že výpis slov bude pomiešaný. U mňa to napr. Vyzeralo takto:

Konzolová aplikácia
ZAKdrohaonjveic smvete

Ak chcete, skúste si z cyklu metódy run() odstrániť krátke uspávanie vlákna. Výstup pravdepodobne bude stále premiešaný, aj keď nie toľko. Je to dané tým, že neuspávané vlákno zvládne za jedno prepnutie kontextu vypísať viac znakov.

My sa ale teraz budeme zaoberať tým, ako docieliť nepomíchaného výstupu. Nevyužijeme k tomu hotové riešenie v podobe metódy println() s tým, že tá predsa taky musí byť nejako synchronizovaná :) . Ak ste do teraz čítali pozorne, mali by ste byť schopní zostaviť riešenie pomocou metód Thread.sleep(), isAlive() alebo join(). Tieto riešenia by však pravdepodobne bola neefektívna a hlavne zbytočne zložitá a neprehľadná. Z tohto dôvodu nám Java ako zvyčajne ponúka komplexné riešenie problému a to práve synchronizáciu.

Veľmi voľné prirovnanie

Skúsme si predstaviť, že naše vlákna sú deti v prvej triede sediaci v krúžku a rozprávajúcej si o tom, čo zažili cez víkend. Ich učiteľka je despota a určila, že hovoriť môže len ten, kto má v ruke jeden konkrétny kamienok. Takže jedno dieťa hovorí a všetky ostatné mlčí. Keď jedno dohovorí, odovzdá kamienok dieťaťu naľavo (alebo napravo - je to jedno, ale snažím sa byť čo najkonkrétnejšie), to dostane povolenie hovoriť a hovorí. To sa opakuje tak dlho, kým všetky deti nepovedia čo chcú.

Viacvláknové aplikácie využívajúce synchronizáciu funguje až na drobné rozdiely rovnako. Tomu kamienka sa hovorí monitor a vlastniť ho môže v jeden okamih iba jedno vlákno. V praxi je monitor iba akýkoľvek ďalší objekt.

Celá synchronizácia je potom realizovaná dvoma spôsobmi:

  1. Uvedením kľúčového slova synchronized v deklarácii hlavičky metódy. Potom je monitorom objekt s touto metódou. V praxi to znamená, že na jednom objekte môže byť naraz vykonávať len jedna jeho synchronizovaná metóda.
  2. Vytvorením vlastného bloku synchronized a externým uvedením monitora. Potom sa kód chová prakticky rovnako ako synchronizovaná metóda. To znamená, že konkrétny synchronizovaný blok môže naraz vykonávať iba jedno vlákno. Výhodnú je väčšia variabilita (zvolíme vlastné monitor, blok môžeme uviesť všade), nevýhodou vyššiu úroveň zložitosti.

Blok synchronized vyzerá takto:

synchronized(monitor) {
    // Synchronizované příkazy
}

Pri oboch prípadoch vlastne okrem iného robíme z niekoľkých príkazov nedeliteľnou (atomické) operáciu. To tiež znamená, že pokiaľ bude vlákno majúce monitor čakať, bude zdržiavať všetky ostatné vlákna čakajúce na monitor.

Ak vlákno narazí na synchronizovaný blok, ale monitor nie je voľný, je zablokované a zaradené do fronty na monitor.

Ak vám niečo stále nie je jasné, nevadí. Synchronizáciou sa totiž budeme zaoberať ďalej, v lekcii Multithreading v Jave - Synchronizácia v praxi :)


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 146x (2.27 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java

 

Predchádzajúci článok
Multithreading v Jave
Všetky články v sekcii
Viacvláknové aplikácie v Jave
Preskočiť článok
(neodporúčame)
Multithreading v Jave - Synchronizácia v praxi
Článok pre vás napísal Matěj Kripner
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Student, programátor v CZ.NIC.
Aktivity