IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 1 - Multithreading v Javě

V tomto článku si uděláme úvod do multithreadingu v Javě. Budu předpokládat, že jste zatím multithreading nevyužívali a mohu tak říci, že všechny vaše dosavadní programy probíhaly lineárně. Tím myslím příkaz po příkazu. Probíhal vždy jen jeden příkaz a aby se mohly vykonat další, musel se tento příkaz dokončit. Hovoříme o tom, že probíhá najednou pouze jedno vlákno (anglicky Thread – viz. dále). Možná jste o tom vláknu ani nevěděli, ale je tam a vstupní bod má v nám dobře známé metodě main(). Tento přístup je sice nejjednodušší, ale často ne nejlepší.

Představte si situaci, kdy jedno vlákno např. čeká na vstup od uživatele. V případě jednovláknového modelu poté čeká celý program! A protože uživatelé jsou zpravidla pomalejší, než náš program, dochází k nepříjemnému mrhání procesorovým časem. Navíc je náš program velice nestabilní. Pokud se s tím naším jedním vláknem cokoli stane (spadne, je zablokováno), bude opět ovlivněn celý program. Může také nastat situace, kdy chceme na určitou dobu nějaké vlákno úmyslně pozastavit. Rozhodně není příjemné, když kvůli tomu musíme pozastavit celý program.

Všechny tyto problémy ale řeší... ano, uhádli jste – multithreading.

Multithreading

Program využívající multithreading se skládá ze dvou a více částí (vláken) a i když se to ze začátku bude možná zdát obtížné, je díky propracované podpoře ze strany Javy vytvoření vícevláknové aplikace celkem snadné. Dá se říci, že vlákno je jakási samostatně se vykonávající posloupnost příkazů. Vlákna můžeme libovolně tvořit (= definovat onu posloupnost příkazů) a spravovat.

Vytvoření vlákna

Prakticky existují 2 způsoby k vytvoření vlákna:

  • Poděděním z třídy Thread
  • Implementací rozhraní Runnable

Tyto dva přístupy jsou si rovnocenné a záleží na každém programátorovi, který z nich si vybere. V tomto článku si ukážeme oba.

Rozhraní Runnable

Rozhraní Runnable je tzv. Funkcionální rozhraní. To je novinka Javy 8 a není to nic jiného, než rozhraní s jednou abstraktní metodou. Později si ukážeme, jaké to přináší výhody. Nicméně zatím pro nás bude důležitější abstraktní metoda run(), kterou toto rozhraní definuje. Tato metoda je pro oba výše uvedené principy společná a představuje právě tu posloupnost příkazů, kterou později vlákno vykonává.

Třída Thread

Třída Thread implementuje rozhraní Runnable a nyní pro nás bude velice důležitá, protože reprezentuje samotné vlákno. Definuje spoustu konstruktorů, z nichž pro nás ale budou zatím důležité jen 4:

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

Jak vidíte, můžeme u vlákna definovat jeho jméno. To je velice užitečná věc, která nám může pomoci při ladění programu. Toto jméno poté můžeme snadno změnit pomocí metody setName(String name), nebo načíst metodou getName().

Druhé dva konstruktory využijeme při vytváření vlákna implementací rozhraní Runnable. Nejdříve vytvoříme objekt typu Runnable, ve kterém implementujeme metodu run() a při vytváření vlákna tento objekt předáme v konstruktoru. Vlákno si předaný objekt uloží a při zavolání metody run() automaticky zavolá jeho metodu run().

Důležitou metodou této třídy je metoda start(), která je jakýmsi vstupním bodem vlákna. Tato metoda provede přípravné práce a poté zavolá metodu run().

Rozšíření třídy Thread

Konečně se tedy dostáváme k samotnému vytvoření vlákna. Založíme si nový projekt a pojmenujeme ho, jak nás zrovna napadne. Nyní vytvoříme třídu 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");
    }
}

Třída dědí ze třídy Thread a překrývá její metodu run(). Jediná nová věc je zde metoda sleep():

public static void sleep(long millis) throws InterruptedException

, kterou definuje třída Thread a která pozastaví vlákno, ze kterého byla volána, na dobu danou argumentem milis. Doba se zadává v milisekundách, což je tisícina sekundy. Nyní se zaměříme na metodu 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");
}

Když nyní spustíme program, bude výstup vypadat nějak takto:

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

Také se ale tento výstup může lišit. Je to samozřejmě způsobeno tím, že vlákna vůči sobě neběží vždy stejně. Jednou může být rychlejší, či lépe řečeno dostat více procesorového času jedno vlákno a podruhé zas jiné. Právě to je na multithreadingu tak zákeřné, že nikdy nevíte, jak budou vlákna mezi sebou přepínána. Tomuto přepínání se říká přepínání kontextu a stará se o něj samotný operační systém.

Přepínání kontextu

Pokud se dvě či více vláken dělí o jeden procesor (nebo lépe řečeno o jedno jádro), musí nějakým způsobem docházet k přepínání mezi jejich prováděním. Jak jsem se již zmínil, o toto přepínání se stará operační systém. Naštěstí ho ale můžeme explicitně ovlivnit i my sami. K rozhodování o tom, kterému vláknu bude dovoleno provádění (kterému bude přidělen procesorový čas) se využívá priority vláken. Každé vlákno má přiřazenou prioritu reprezentovanou číslem od 1 do 10. Výchozí hodnota je 5. Prioritu můžeme nastavit metodou setPriority(), nebo načíst metodou getPriority().

Dá se říci, že vlákno s vyšší prioritou má přednost v provádění před vláknem s nižší prioritou a kdykoli si může vynutit jeho pozastavení ve svůj prospěch (silnější přežije :) ). Jak jsem ale říkal, o přepínání se stará operační systém a každý OS může s vlákny a jejich prioritou nakládat jinak. Proto byste neměli spoléhat jen na automatické přepínání a snažit se alespoň trochu přepínání hlídat. Např. platí, že je potřeba zařídit, aby se vlákna se shodnou prioritou jednou za čas sama vzdala řízení. To můžete elegantně způsobit statickou metodou yield() na třídě Thread, která „vezme“ řízení aktuálně běžícímu vláknu a předá ho čekajícímu vláknu s nejvyšší prioritou.

Implementace rozhraní Runnable

Druhým způsobem vytvoření vlákna je implementace rozhraní Runnable. Jak už jsem říkal, toto rozhraní je tzv. funkcionální rozhraní a tudíž obsahuje jen jednu abstraktní metodu. V tomto případě je to samozřejmě metoda run(). Udělejme si tedy malou odbočku k Javě 8.

Funkcionální rozhraní a lambda výrazy

Funkcionální rozhraní je novinka Javy 8 a je to takové rozhraní, které má jen jednu abstraktní metodu. Zároveň by pro přehlednost mělo být označeno anotací @FunctionalInterface.

Představme si, že chceme např. vytvořit objekt typu Comparator. Ve starších verzích bychom museli postupovat jako při tvorbě abstraktní třídy:

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

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

Sami musíte uznat, že tolik kódu pro tak jednoduchou operaci není příliš výhodné. Proč vlastně musíme psát kterou metodu překrýváme, když rozhraní má jen jednu? Naštěstí to ale již není nutné. Od Javy 8 tak můžeme to samé napsat takto pomocí lambda výrazu:

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

Jednoduše uvedeme parametry abstraktní metody, operátor ->, a blok kódu (implementaci abstraktní metody). Pokud je však v tomto bloku jen jeden příkaz, můžeme vypustit složené závorky i příkaz return. Také je možné vypustit datový typ parametrů. Ten samý kód tak může vypadat takto:

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

Krása ne? A kdyby byl v závorce jen jeden parametr, mohli bychom i ty závorky vypustit.

Pro případné zájemce o více informací tu mám link a jeden úžasný a podrobný článek o Javě 8. Pro ty, co se s angličtinou moc nekamarádí mám jeden už ne tak dobrý článek :)

Nyní již tedy víme, co je to funkcionální rozhraní a můžeme lehce vytvořit vlákno pomocí implementace rozhraní Runnable. Pamatujete ještě na druhé dva konstruktory třídy Thread? Ty tu využijeme. Do metody main() nyní místo vytvoření třídy Vlakno umístěte 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");

Tady by mělo být vše jasné. Program by se měl chovat stejně jako před modifikací. Možná se vám to bude zdát trochu narychlo řešení, asi by bylo v tomto případě lepší použít koncept abstraktní třídy namísto lambda. Ale to už je na vás.

Budu se na vás těšit u další lekce Multithreadingu v Javě, Multithreading v Javě - Daemon, join a synchronized.


 

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 262x (2.42 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

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