1. díl - Multithreading v Javě

Java Vlákna 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:

static 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.err.println("Nepodařilo se uspat vlákno: " + ex);
            }
        }
        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.err.println("Nepodařilo se uspat hlavní vlákno: " + ex);
        }
    }
    System.out.println("Hlavní vlákno ukončeno");
}

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

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í @FunctionalIn­terface.

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("Druhé vlákno spuštěno");
    for(int i = 0; i < 4; i++) {
        System.out.println("Druhé vlákno: " + i);
        try {
            Thread.sleep(500);
        } catch (InterruptedException ex) {
            System.err.println("Nepodařilo se uspat vlákno: " + ex);
        }
    }
    System.out.println("Druhé vlákno 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šího dílu Multithreadingu v Javě.


 

Stáhnout

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

 

  Aktivity (1)

Článek pro vás napsal Matěj Kripner
Avatar
Autor se převážně věnuje programování a dalším věcem, které považuje za důležité.

Jak se ti líbí článek?
Celkem (9 hlasů) :
55555


 



 

 

Komentáře

Avatar
Hartrik
Redaktor
Avatar
Hartrik:

Rozhraní podporující lambda výrazy nemusí být označeno anotací @FunctionalIn­terface. Slouží jen pro kontrolu, jako např. @Override.

 
Odpovědět 30.8.2014 12:47
Avatar
Matěj Kripner
Redaktor
Avatar
Odpovídá na Hartrik
Matěj Kripner:

Máš pravdu, opravím to. Díky

Odpovědět  +1 30.8.2014 14:07
"We reject kings, presidents and voting. We believe in rough consensus and running code" David Clark
Avatar
mara
Člen
Avatar
mara:

Matěj Kripner díky moc :) :D doufám, že tvé články neskončí jen u Vláken :) Byl bych rád, kdyby jsi ještě pokračoval :)

Odpovědět  +1 31.8.2014 17:14
Co na srdci, to na Facebooku
Avatar
Matěj Kripner
Redaktor
Avatar
Odpovídá na mara
Matěj Kripner:

Jsem rád, že jsem mohl pomoct :) Určitě hodlám pokračovat.

Odpovědět  +2 31.8.2014 17:42
"We reject kings, presidents and voting. We believe in rough consensus and running code" David Clark
Avatar
mara
Člen
Avatar
mara:

Matěj Kripner o tomhle jsem třeba věděl, ale pomohlo mi to se zdokonalit. Ono by aji pomohlo vypsat věci, které v Javě jsou a dost se používají. Jinak bych neměl ani nejmenší tušení, že nějaká vlákna jsou a tím pádem bych to ani nikde nehledal.

Odpovědět 31.8.2014 17:56
Co na srdci, to na Facebooku
Avatar
Roman
Člen
Avatar
Roman:

Perfektný článok, v škole som mal vždy problém pochopiť túto problematiku a sem je to dá sa povedať napísané aj pre tých menej chápavých ako som ja :)) Konečne mám pocit že aj ja tomu troška rozumieť, veľká vďaka len tak ďalej :)

 
Odpovědět  +1 17.8.2015 10:47
Avatar
Avev Frger
Člen
Avatar
Avev Frger:

Pises "Funkcionální rozhraní je novinka Javy 8 a je to takové rozhraní, které má jen jednu abstraktní metodu" no napr. Comparator ma metod viac a je tiez Functional Interface.

 
Odpovědět 12. srpna 23:40
Avatar
Atrament
Člen
Avatar
Odpovídá na Avev Frger
Atrament:

Comparator má jenom jednu abstraktní metodu a tou je compare, equals(Object) odpovídá stejné metodě ve tříde Object, takže se nepočítá. Všechny ostatní metody v Comparatoru jsou buď statické nebo defaultní, takže se taky nepočítají. Viz http://www.lambdafaq.org/…l-interface/

 
Odpovědět  +1 13. srpna 0:09
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 8 zpráv z 8.