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

Třída Thread

Nejjednodušší práce s vlákny je přes třídu 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:

  1. 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.
  2. Java umožňuje mít jednu třídu jako předka, takto si tuto možnost zabijeme.
  3. 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 yeild() 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 join( jineVlakno ) 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 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 zde.

Příklady na procvičení

  1. 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.
  2. Modifikujte předchozí program tak, aby vypsal počet vypsaných čísel po ukončení běhu vláken.
  3. Naprogramujte synchronizovanou prioritní frontu. Vyzkoušejte činnost programu se třemi vlákny a porovnejte ji s nesynchronizovanou variantou.
  4. 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.
courses/b0b36pjv/tutorials/09/vlakna.txt · Last modified: 2018/04/17 22:24 by seredlad