Zarábaj až 6 000 € mesačne! Akreditované rekvalifikačné kurzy od 0 €. Viac informácií.

1. diel - Multithreading v Jave

V tomto článku si urobíme úvod do multithreadingu v Jave. Budem predpokladať, že ste zatiaľ multithreading nevyužívali a môžem tak povedať, že všetky vaše doterajšie programy prebiehali lineárne. Tým myslím príkaz po príkaze. Prebiehal vždy len jeden príkaz a aby sa mohli vykonať ďalšie, musel sa tento príkaz dokončiť. Hovoríme o tom, že prebieha naraz iba jedno vlákno (anglicky Thread - viď. Ďalej). Možno ste o tom vláknu ani nevedeli, ale je tam a vstupný bod má v nám dobre známej metóde main(). Tento prístup je síce najjednoduchšie, ale často nie najlepší.

Predstavte si situáciu, kedy jedno vlákno napr. Čaká na vstup od užívateľa. V prípade jednovláknového modeli potom čaká celý program! A pretože používatelia sú spravidla pomalší, než náš program, dochádza k nepríjemnému mrhanie procesorovým časom. Naviac je náš program veľmi nestabilná. Ak sa s tým naším jedným vláknom čokoľvek stane (spadne, je zablokované), bude opäť ovplyvnený celý program. Môže tiež nastať situácia, kedy chceme na určitú dobu nejaké vlákno úmyselne pozastaviť. Rozhodne nie je príjemné, keď kvôli tomu musíme pozastaviť celý program.

Všetky tieto problémy ale riešia ... áno, uhádli ste - multithreading.

Multithreading

Program využívajúce multithreading sa skladá z dvoch a viacerých častí (vlákien) a aj keď sa to zo začiatku bude možno zdať ťažké, je vďaka prepracovanej podpore zo strany Javy vytvorenie viacvláknové aplikácie celkom jednoduché. Dá sa povedať, že vlákno je akási samostatne sa vykonávajúci postupnosť príkazov. Vlákna môžeme ľubovoľne tvoriť (= definovať onú postupnosť príkazov) a spravovať.

Vytvorenie vlákna

Prakticky existujú 2 spôsoby na vytvorenie vlákna:

  • Poděděním z triedy Thread
  • Implementáciou rozhranie Runnable

Tieto dva prístupy sú si rovnocenné a záleží na každom programátorovi, ktorý z nich si vyberie. V tomto článku si ukážeme obaja.

Rozhranie Runnable

Rozhranie Runnable je tzv. Funkcionálna rozhranie. To je novinka Javy 8 a nie je to nič iné, než rozhranie s jednou abstraktné metódou. Neskôr si ukážeme, aké to prináša výhody. Avšak zatiaľ pre nás bude dôležitejšie abstraktné metóda run(), ktorú toto rozhranie definuje. Táto metóda je pre oba vyššie uvedené princípy spoločná a predstavuje práve tú postupnosť príkazov, ktorú neskôr vlákno vykonáva.

Trieda Thread

Trieda Thread implementuje rozhranie Runnable a teraz pre nás bude veľmi dôležitá, pretože reprezentuje samotné vlákno. Definuje veľa konstruktoru, z ktorých pre nás ale budú zatiaľ dôležité len 4:

public Thread()
public Thread(String name)
public Thread(Runnable target)
public Thread(Runnable target, String name)

Ako vidíte, môžeme u vlákna definovať jeho meno. To je veľmi užitočná vec, ktorá nám môže pomôcť pri ladení programu. Toto meno potom môžeme ľahko zmeniť pomocou metódy setName(String name), alebo načítať metódou getName().

Druhé dva konštruktory využijeme pri vytváraní vlákna implementáciou rozhrania Runnable. Najskôr vytvoríme objekt typu Runnable, v ktorom implementujeme metódu run() a pri vytváraní vlákna tento objekt odovzdáme v konstruktoru. Vlákno si odovzdaný objekt uloží a pri zavolaní metódy run() automaticky zavolá jeho metódu run().

Dôležitou metódou tejto triedy je metóda start(), ktorá je akýmsi vstupným bodom vlákna. Táto metóda vykoná prípravné práce a potom zavolá metódu run().

Rozšírenie triedy Thread

Konečne sa teda dostávame k samotnému vytvoreniu vlákna. Založíme si nový projekt a pomenujeme ho, ako nás práve napadne. Teraz vytvoríme triedu Vlakno:

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

Trieda dedí z triedy Thread a prekrýva jej metódu run(). Jediná nová vec je tu metóda sleep():

public static void sleep(long millis) throws InterruptedException

, Ktorú definuje trieda Thread a ktorá pozastaví vlákno, z ktorého bola volaná, na dobu danú argumentom Milis. Doba sa zadáva v milisekundách, čo je tisícina sekundy. Teraz sa zameriame na metódu main():

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

Keď teraz spustíme program, bude výstup vyzerať nejako takto:

Konzolová aplikácia
Hlavní vlákno spuštěno
Hlavní vlákno: 0
Vlákno Druhe spuštěno
Vlákno Druhe: 0
Vlákno Druhe: 1
Hlavní vlákno: 1
Vlákno Druhe: 2
Hlavní vlákno: 2
Vlákno Druhe: 3
Vlákno Druhe ukončeno
Hlavní vlákno: 3
Hlavní vlákno ukončeno

Tiež sa ale tento výstup môže líšiť. Je to samozrejme spôsobené tým, že vlákna voči sebe nebeží vždy rovnako. Raz môže byť rýchlejší, či lepšie povedané dostať viac procesorového času jedno vlákno a druhýkrát zas iné. Práve to je na multithreadingu tak zákerné, že nikdy neviete, ako budú vlákna medzi sebou prepínané. Tomuto prepínanie sa hovorí prepínanie kontextu a stará sa o neho samotný operačný systém.

Prepínanie kontextu

Ak sa dve či viac vlákien delia o jeden procesor (alebo lepšie povedané o jedno jadro), musí nejakým spôsobom dochádzať k prepínanie medzi ich vykonávaním. Ako som sa už zmienil, o toto prepínanie sa stará operačný systém. Našťastie ho ale môžeme explicitne ovplyvniť aj my sami. Na rozhodovanie o tom, ktorému vláknu bude dovolené implementácie (ktorému bude pridelený procesorový čas) sa využíva priority vlákien. Každé vlákno má priradenú prioritu reprezentovanú číslom od 1 do 10. Predvolená hodnota je 5. Prioritu môžeme nastaviť metódou setPriority(), alebo načítať metódou getPriority().

Dá sa povedať, že vlákno s vyššou prioritou má prednosť v realizácii pred vláknom s nižšou prioritou a kedykoľvek si môže vynútiť jeho pozastavenie vo svoj prospech (silnejší prežije :) ). Ako som ale hovoril, o prepínanie sa stará operačný systém a každý OS môže s vláknami a ich prioritou nakladať inak. Preto by ste nemali spoliehať len na automatické prepínanie a snažiť sa aspoň trochu prepínanie strážiť. Napr. platí, že je potrebné zariadiť, aby sa vlákna sa zhodnú prioritou raz za čas sama vzdala riadenia. To môžete elegantne spôsobiť statickou metódou yield() na triede Thread, ktorá "vezme" riadenie aktuálne bežiacemu vláknu a odovzdá ho čakajúcemu vláknu s najvyššou prioritou.

Implementácia rozhrania Runnable

Druhým spôsobom vytvorenie vlákna je implementácia rozhrania Runnable. Ako som už hovoril, toto rozhranie je tzv. Funkcionálne rozhrania a teda obsahuje len jednu abstraktné metódu. V tomto prípade je to samozrejme metóda run(). Urobme si teda malú odbočku k Jave 8.

Funkcionálne rozhranie a lambda výrazy

Funkcionálne rozhranie je novinka Javy 8 a je to také rozhranie, ktoré má len jednu abstraktné metódu. Zároveň by pre prehľadnosť malo byť označené anotácií @FunctionalInterface.

Predstavme si, že chceme napr. Vytvoriť objekt typu Comparator. V starších verziách by sme museli postupovať ako pri tvorbe abstraktné triedy:

Comparator<String> com = new Comparator<String>() {

    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
};

Sami musíte uznať, že toľko kódu pre tak jednoduchú operáciu nie je príliš výhodné. Prečo vlastne musíme písať ktorú metódu prekrývame, keď rozhranie má len jednu? Našťastie to ale už nie je nutné. Od Javy 8 tak môžeme to isté napísať takto pomocou lambda výrazu:

Comparator<String> com = (String a, String b) -> {
    return b.compareTo(a);
};

Jednoducho uvedieme parametre abstraktné metódy, operátor ->, a blok kódu (implementáciu abstraktné metódy). Ak je však v tomto bloku len jeden príkaz, môžeme vypustiť zložené zátvorky aj príkaz return. Tiež je možné vypustiť dátový typ parametrov. Ten istý kód tak môže vyzerať takto:

Comparator<String> com = (a, b) -> b.compareTo(a);

Krása nie? A keby bol v zátvorke len jeden parameter, mohli by sme aj tie zátvorky vypustiť.

Pre prípadných záujemcov o viac informácií tu mám link a jeden úžasný a podrobný článok o Jave 8. Pre tých, čo sa s angličtinou moc nekamaráti mám jeden už nie tak dobrý článok :)

Teraz už teda vieme, čo je to funkcionálne rozhranie a môžeme ľahko vytvoriť vlákno pomocou implementácie rozhrania Runnable. Pamätáte ešte na druhej dva konštruktory triedy Thread? Tie tu využijeme. Do metódy main() teraz namiesto vytvorenia triedy Vlakno umiestnite tento kód:

Thread mojeVlakno = new Thread(() -> {
    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");
}, "Druhe");

Tu by malo byť všetko jasné. Program by sa mal správať rovnako ako pred modifikáciou. Možno sa vám to bude zdať trochu narýchlo riešenie, asi by bolo v tomto prípade lepšie použiť koncept abstraktné triedy namiesto lambda. Ale to už je na vás.

Budem sa na vás tešiť u ďalšej lekcie multithreading v Jave, Multithreading v Jave - Daemon, join a synchronized .


 

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é 269x (2.42 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java

 

Všetky články v sekcii
Viacvláknové aplikácie v Jave
Preskočiť článok
(neodporúčame)
Multithreading v Jave - Daemon, join a synchronized
Č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