====== Vlákna v Javě ====== Vlákna přináší možnost multitaskového prostředí do Javy. Multitasking znamená možnost "souběžné" práce několika procesů. Slouží ke zrychlení programu, který nemusí čekat na ukončení dlouhých operací typu přístupu na síť a může sbírat další potřebné informace k výpočtu a výpočet provést až vlákno zpracuje požadavek. ==== Stavy vlákna ==== {{:courses:b6b36pjv:tutorials:09:stavy_vlaken.jpg?nolink&700|}} ==== Třída Thread ==== Nejjednodušší práce s vlákny je přes třídu [[http://java.sun.com/j2se/1.4.2/docs/api/java/lang/Thread.html|Thread]]. Nejdůležitější metodou této třídy je metoda **run()**, která specifikuje chování vlákna jako takového. Zde je uveden příslušný kód: /** * Jednoduché vlakno */ public class JednoducheVlakno extends Thread { private int _count = 5; /** * Konstruktor musi obsahovat start() */ public JednoducheVlakno() { super("Jmeno Vlakna"); start();// Ostartuje beh vlakna } /** * Telo vlakna */ public void run() { while( true ) { System.out.println(_count); if (_count-- == 0) return; } } } Problémy: - My chceme vytvořit vlákno, nikoli nový typ vlákna. Potomek Threadu by mělo být vlákno, tedy něco, co má novou funkčnost pro chování vlákna. - Java umožňuje mít jednu třídu jako předka, takto si tuto možnost zabijeme. - Je nesmyslné startovat vlákno v jeho konstruktoru. Lepší implementace je pomocí rozhraní **Runnable**. public class Vlakno implements Runnable{ int sdilena = 0; public void run() { for(int i = 0; i<100;i++){ System.out.printf("Sdilena:%5d, i:%5d%n", sdilena++,i); //System.out.println("Sdilena:" + sdilena + " i: " +i); } } } Zde je demo, pro spuštění je nutné definovat main, nejlépe v jiné třídě. public class TestSdileni { public static void main(String[] args) { Vlakno v1 = new Vlakno(); Thread t1 = new Thread(v1), t2 = new Thread(v1); //obě vlákna musí být nad stejným objektem typu Runnable - zde v1 t2.start(); t1.start(); } } Zkuste spustit předchozí příklad. Jeden z možných výstupů je tento: Sdilena: 0, i: 0 Sdilena: 2, i: 1 Sdilena: 3, i: 2 Sdilena: 4, i: 3 Sdilena: 5, i: 4 Sdilena: 6, i: 5 Sdilena: 7, i: 6 Sdilena: 8, i: 7 Sdilena: 9, i: 8 Sdilena: 10, i: 9 Sdilena: 11, i: 10 Sdilena: 12, i: 11 Sdilena: 13, i: 12 Sdilena: 14, i: 13 Sdilena: 15, i: 14 Sdilena: 16, i: 15 Sdilena: 17, i: 16 Sdilena: 1, i: 0 Sdilena: 19, i: 1 Sdilena: 20, i: 2 Zdůvodněte proč posloupnost Sdilene začíná 0,2,.. nikoli 0,1,2,... Vlákno skončí svoji činnost, pokud je ukončena metoda **run()**. Vlákna sdílejí společný procesorový čas a tak pokud běží na jedno-procesorovém počítači jedno vlákno jsou ostatní vlákna suspendována a čekají na přidělení CPU. Přidělování procesoru řídí plánovač úloh(Scheduler) a aby vlákno nezabíralo příliš času na CPU lze informovat plánovač statickou metodou **yield()** o tom, že vlákno dospělo do částečného výpočtu a může být suspendováno. Volání této metody samozřejmě automaticky nevede na přerušení činnosti vlákna, ale zvyšuje pravděpodobnost této akce. Je nutné uvést, že plánovač je zde míněn na úrovni JVM, tedy, aby vlákno běželo, musí běžet (být naplánována) JVM a v ní musí být toto vlákno naplánováno. Tedy všechna vlákna v rámci jednoho programu sdílí jeden čas přidělovaný OS. === Metoda sleep() === Další důležitou metodu, kterou přináší třída Thread je metoda **sleep()**, která přeruší činnost vlákna na dobu specifikovanou argumentem této funkce. Časovou jednotkou argumentu je milisekunda. try { sleep( 100 ); } catch ( InterruptedExeception e) { //... Osetreni vyjimky peruseni } === Priorita vlákna === Priorita je určena číselnou hodnotou a výše této hodnoty určuje, jak často dostane vlákno přiděleno procesorový čas. Java má 10 priorit, ale jsou zde problémy s multiplatfomností, jelikož třeba OS Windows má pouze 7 priorit a tak se provádí rekalkulace. Jediné bezpečné použití priorit přináší konstanty MAX_PRIORITY, MIN_PRIORITY a NORM_PRIORITY. Samotné nastavení se provádí metodou třídy Thread **setPriority( int )**. Aktuální hodnota priority se dá získat metodou int **getPriority()**. === Vytváření daemonů === Každé vlákno existuje jen po dobu existence svého rodiče a ten se neukončí, dokud alespoň jedno nedémonické vlákno běží. Pokud chceme, aby se hlavní program ukončil nezávisle na nějakém vlákně, tak toto vlákno nastavíme jako Deamona. To se provádí metodou **setDeamon( boolean )**, která musí být zavolána před spuštění metody **start()**. Všechny vlákna vytvořená Deamonem jsou také deamoni a pokud chci o nějakém vláknu zjistit, jestli je deamonem, tak použiji metodu **isDeamon()**. === Řetězení vláken === Do provádění vláken lze zavést jistý pořádek a tak dosáhnout určité návaznosti. Pokud nějaké vlákno potřebuje výsledné informace, které zpracovává jiné vlákno, tak se pomocí metody **jineVlakno.join()** uspí, dokud není vlákno zpracovávající informaci ukončeno. Informaci o ukončeni vlákna lze přečíst metodou **isAlive()**. ==== Synchronizace vláken ==== Pokud se může stát, že bude k jednomu objektu přistupovat více vláken s různými metodami měnícími její obsah, je třeba zaručit konzistenci objektu mezi jednotlivými operacemi tj. exklusivitu práce s objektem. Aby byl přístup exkluzivní, tak se u objektů zavádí zámek, kterým si přistupující vlákno zamkne objekt před interferencí s ostatními. Pokud chce vlákno pracovat se zamčeným objektem, tak je odloženo do uvolnění objektu. Tento zámek se zavádí u metod pracující s kritickým objektem klíčovým slovem synchronized. /** * pouziti synchronized */ public class SynchronizedEvenGen { int i = 0; synchronized void next() {i++; i++;} synchronized int getVal() { return i;} .... } Nejen metody lze synchronizovat, protože některé operace mohou být velice rozsáhlé a tak lze vytvářet i tzv. kritické sekce, kde se vyskytují operace se sdíleným objektem. Do těchto sekcí lze vstoupit jen tehdy, pokud získáme klíč od specifikovaného objektu. /** * pouziti kriticke sekce */ public void velkaMetoda() { ... synchronized (synObject) { synObject.op1(); } .... } Zámek kritické sekce se uvolní: * automaticky po provedení posledního příkazu synchronizovaného bloku nebo metody * na žádost programu v kritické sekci voláním metody wait() Metody: * **wait()** - uspí vlákno buď na čas uvedení jako argument nebo neomezeně, pokud je metoda bez argumentu. Stejně jako metoda **sleep()** může být přerušena metodou **interrupt()** a tak je třeba ošetřit vyjímku [[http://java.sun.com/j2se/1.3/docs/api/java/lang/InterruptedException.html|InterruptedException]]. * **notify()** - probouzí konkrétní vlákno * **notifyAll()** - probouzí všechny spící vlákna Všechny tyto metody lze použít pouze za předpokladu, že objekt vlastní klíč od příslušného vlákna, tedy uvnitř synchronizovaného bloku nebo metody. === Uváznutí (deadlock) === Žádost o uzamčení další kritické sekce bez odemčení předchozí může vést k nebezpečné situaci, kdy jsou všechna vlákna podílející se na přístupu k těmto sdíleným zdrojům pozastavena (blokována čekáním na uvolnění některého sdíleného prostředku) a proces se zablokuje. Bez dodatečných opatření se vlákna ze zablokovaného stavu sama nemohou uvolnit. Ochrana před uváznutím - zachovat shodné pořadí žádostí o sdílené prostředky ve všech zúčastněných vláknech ==== Příklad: Producent - Konzument ==== Jedno vlákno produkuje data (producent) a druhé vlákno tato data spotřebovává (konzument). Konzument nemůže spotřebovat více dat než producent vyprodukuje. V případě, že jsou všechna právě vyprodukovaná data spotřebována musí konzument čekat. Naopak producent může vyprodukovat jen tolik dat, kolik je konzument schopen spotřebovat (včetně obsahu případné vyrovnávací paměti). Pokud není vyprodukovaná data kam ukládat musí producent čekat, dokud konzument data nespotřebuje. public class Main { public static void main(String[] args) { Vycep s = new Vycep(10); Producent p1 = new Producent(s,1); Konzument k1 = new Konzument(s,1); Thread t1 = new Thread(p1); Thread t2 = new Thread(k1); t1.start(); t2.start(); try { // necham bezet 10 sec Thread.sleep(10 * 1000); } catch (InterruptedException ex) { System.out.println("Main: Doslo k preruseni."); } s.setVycepOtevren(false); try { t1.join(); t2.join(); } catch (InterruptedException ex) { System.out.println("Main: Doslo k preruseni."); } System.out.println("Program ukoncen"); } } public class Producent implements Runnable { private Vycep s; private int counter; private int cislo; public Producent(Vycep s, int cislo) { this.s = s; this.cislo = cislo; counter = 0; } public void run() { while (s.isVycepOtevren()) { int polozka = counter++; System.out.println("Vycepni" + cislo + ": Chysta se tocit " + polozka + ". pivo."); s.put(polozka); Thread.yield(); } } } public class Konzument implements Runnable { private Vycep s; private int cislo; public Konzument(Vycep s, int cislo) { this.s = s; this.cislo = cislo; } public void run() { while (s.isVycepOtevren()) { System.out.println("Konzument" + cislo + ": Vypil pivo " + s.get()); Thread.yield(); } } } public class Vycep { private int items[]; private int top, bottom; private boolean full; private boolean vycepOtevren; public Vycep(int size) { items = new int[size]; top = bottom = 0; vycepOtevren = true; } synchronized public void put(int item) { try { while (full && isVycepOtevren()) { System.out.println("Dosly sklenice, cekam..."); wait(); } items[top] = item; System.out.println("Natoceno pivo " + item); top++; top %= items.length; if (top == bottom) full = true; notifyAll(); } catch (InterruptedException e) { System.out.println("Vycep: Doslo k preruseni."); } } synchronized public int get() { int result = -1; try { while(top == bottom && !full && isVycepOtevren()) // empty { System.out.println("Doslo pivo, cekam..."); wait(); } result = items[bottom]; System.out.println("Zakaznik vzal pivo " + result); bottom++; bottom %= items.length; full = false; notifyAll(); } catch (InterruptedException e) { System.out.println("Vycep: Doslo k preruseni."); } return result; } synchronized public boolean isVycepOtevren() { return vycepOtevren; } synchronized public void setVycepOtevren(boolean vycepOtevren) { this.vycepOtevren = vycepOtevren; notifyAll(); } } Šablonu pro úkol typu „výčep“ s implementovanou kruhovou frontou si můžete stáhnout {{ :courses:b0b36pjv:tutorials:09:lab09pubthreads.zip | zde}}. ===== Příklady na procvičení ===== - Napište program, který spustí dvě vlákna, která budou generovat náhodná čísla od jedné do tisíce a tato čísla budou vypisovat spolu s pořadím, které určí, které vlákno kolikáté číslo vypisuje. Vlákno skončí pokud vygenerované číslo je menší než číslo 5. Po ukončení obou vláken program vypíše hlášení: běh vláken ukončen. - Modifikujte předchozí program tak, aby vypsal počet vypsaných čísel po ukončení běhu vláken. - Naprogramujte synchronizovanou prioritní frontu. Vyzkoušejte činnost programu se třemi vlákny a porovnejte ji s nesynchronizovanou variantou. - Naprogramujte Hospodu, která bude mít více stolů (pět) a vlákna reprezentující hosty a obsluhu. Hosté přicházejí, sedají si k volným stolům a objednávají si pivo. Každý host má svou vlastní frekvenci pití, obsluha unese různé počty piv. Simulujte pití pro plnou hospodu a jednoho líného číšníka a pro plnou hospodu a rychlou obsluhu.