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