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 3 - Multithreading v Javě - Synchronizace v praxi

V minulé lekci, Multithreading v Javě - Daemon, join a synchronized, jsme nakousli téma synchronizace.

Dnes se podíváme na využití synchronizace v praxi.

Přepínání kontextu

O přepínání kontextu jsme již mluvili v prvním díle. Jedná se o způsob, jakým procesor přepíná mezi jednotlivými vlákny běžícími na jednom procesorovém jádru. A právě kvůli tomuto přepínání se musíme starat o synchronizaci. Jak jsem již zmiňoval, nikdy totiž nemůžeme přesně vědět, kdy k přepnutí dojde. Pojďme si to demonstrovat na příkladu:

public class Prepinac {

    public void vypisuj0() {
        while (true) {
            System.out.print("0");
        }
    }

    public void vypisuj1() {
        while (true) {
            System.out.print("1");
        }
    }

    public void prepinej() {
        Thread vlakno = new Thread(this::vypisuj0);
        vlakno.start();
        vypisuj1();
    }
}

Vytvořili jsme třídu Prepinac s třemi metodami:

První 2 metody třídy jsou velmi jednoduché a simulují nějakou delší činnost. První metoda do nekonečna vypisuje nuly do konzole, druhá metoda stejným způsobem vypisuje jedničky.

Metoda prepinej() je pro nás již zajímavější. Sama spustí výpis jedniček, ale ještě předtím vytvoří nové vlákno, kterému přiřadí metodu pro výpis nul. Toto vlákno poté také spustí.

Metodu main si poupravte, aby vytvořila objekt Prepinace a zavolala na něm metodu prepinej().

Když nyní program spustíme, nejspíše se vám nic nevypíše. Celý výpis se zobrazí až po ukončení programu. Proč? Jednoduše proto, že konzole příchozí znaky nevypisuje, ale láduje je do vyrovnávací paměti, kterou vypíše až po ukončení řádku. Nicméně dříve či později se náš binární koktejl ukáže. U mě vypadal takto:

Konzolová aplikace
11111100000000000000000000000000000111111111111111111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000001
11111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000
00000000000000000000000000000000000000111111111111111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
11111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000
00000000000000000000000000000011101000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000001001111111111110000000000000001111111111111111000000001000001111111000000011000000000111
11100000000000001111111111111111111111111111111111111111111111111111111111111100000000000000000000000000
11100100000000000000000000000000000000000000000011111111111111111111111111111111111111111111111111111111
11111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000100000000000
00000000000000000000000000000000000000001111000000000000000000000000000000000000000000000000000000000000
00000000001110000000111111111111111111111111111111111111111111111111111111111111111111111111000011111100
00000000000000000000000000011000000000000000000000000000000001111111111111111111111111111111111111111111
11111111111111110000001111111111111111111111111111111111111100000000000000000000000000000001111111111111
11111111111111111111110001111100000000000001111111111111111111111111111111111111111111111111111111111110
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111
11111111111111111111100001111111111111111111001000001111111111111111111111111111111111111111111111111111
11111111111000000000000000000000000000000000000000000000000000000000000000000011110000111111111110000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111000000000
00001111100000011111100000000000000011001111111111111111111111111111111111111111111111111111111111111111
1111111111111111111110000000000000000000000000000000000000000000000000000
...

Výpis samozřejmě bude vypadat při každém spuštění jinak.

Všimněte si, že jedničky a nuly nejsou úplně na střídačku, jak bychom mohli očekávat. Je vidět, jak vlákno chvíli běží a poté je uspáno. Intervaly se také liší, i když průměrně běží obě vlákna stejně dlouho. Myslím, že nemusím připomínat, že je to dáno přepínáním kontextu. Pojďme se ale zamyslet nad tím, jak takové přepínání může ovlivnit reálnou aplikaci.

Naprogramujeme si třídu reprezentující bankomat, který eviduje nějakou hotovost.

ThreadSafety

Vlákno může přistupovat k instančním nebo statickým proměnným. Právě toto je jeden ze způsobů, kterými spolu mohou jednotlivá vlákna komunikovat. Jak již tušíme, háček bude v již zmíněné synchronizaci. Představme si následující třídu:

class BankomatUnsafe {
    private int hotovost = 100;

    private void vyber100() {
        if (hotovost >= 100) {
                System.out.println("Vybírám 100");
                hotovost -= 100;
                System.out.printf("na účtu máte ještě %d.%n", hotovost);
        }
    }

    public void vyberVlakny() {
        Thread vlakno1 = new Thread(this::vyber100);
        vlakno1.start();
        vyber100();
            if (hotovost < 0)
                System.out.println("Hotovost je v mínusu, okradli nás.");
    }

}

Třída reprezentuje bankomat, který eviduje nějakou hotovost. Ta je při vytvoření bankomatu 100 Kč. Dále disponuje jednoduchou metodou vyber100(), která vybere 100 korun v případě, že je na účtu potřebný zůstatek. Zajímavá je pro nás metoda vyberVlakny(), která se pomocí 2 vláken (aktuálního a nově vytvořeného) pokusí vybrat 100 Kč. Pokud se s hotovostí náhodou dostaneme do mínusu, vypíšeme o tom hlášení.

V kódu jsem pro vytvoření objektu typu Runnable použil odkaz na instanční metodu vyber100() pomocí operátoru ::. To je novinka Javy 8 a ty, kteří nevědí o co se jedná, znovu odkáži na článek o změnách provedených novou verzí Javy.

Do hlavní metody přidáme kód, který provede 200 výběrů na 100 bankomatech:

for (int i = 0; i < 100; i++) {
    BankomatUnsafe bankomat = new BankomatUnsafe();
    bankomat.vyberVlakny();
}

A aplikaci spustíme:

Konzolová aplikace
Vybírám 100
Vybírám 100
na účtu máte ještě 0.
na účtu máte ještě -100.
Hotovost je v mínusu, okradli nás.
Vybírám 100
na účtu máte ještě 0.
Vybírám 100
na účtu máte ještě 0.
Vybírám 100
na účtu máte ještě 0.
Vybírám 100
na účtu máte ještě 0.
Vybírám 100
Vybírám 100
na účtu máte ještě 0.
Hotovost je v mínusu, okradli nás.
na účtu máte ještě -100.
Vybírám 100
...

Z výpisu vidíme, že něco nesedí. Kde je problém?

V metodě vyber100() kontrolujeme podmínkou, zda je na účtu dostatečná hotovost. Představte si, že je na účtu 100 Kč. Podmínka tedy platí a systém vlákno uspí třeba ihned za vyhodnocením podmínky. Toto vlákno tedy čeká. Druhé vlákno také zkontroluje podmínku, která platí, a odečte 100 Kč. Poté se probudí první vlákno, které je již za podmínkou a také odečte 100 Kč. Ve výsledku máme na účtu tedy záporný zůstatek!

Vidíme, že práce s vlákny přináší nová úskalí, se kterými jsme se doposud ještě nesetkali. Situaci vyřešíme pomocí synchronizace.

Synchronizace

Jistě se shodneme na tom, že sekce s ověřením zůstatku a jeho následnou změnou musí proběhnout vždy celá, jinak se dostáváme do výše zmíněné situace. Problém vyřešíme tím, že sekci, kde se sdílenou proměnnou hotovost pracujeme, obalíme blokem synchronized. Kód třídy bankomatu upravíme do následující podoby:

private int hotovost = 100;
    private final Object monitor = new Object();

    private void vyber100() {
        synchronized (monitor) {
            if (hotovost >= 100) {
                System.out.println("Vybírám 100");
                hotovost -= 100;
                System.out.printf("na účtu máte ještě %d.%n", hotovost);
            }
        }
    }

    public void vyberVlakny() {
        Thread vlakno1 = new Thread(this::vyber100);
        vlakno1.start();
        vyber100();
        if (hotovost < 0) {
            System.out.println("Hotovost je v mínusu, okradli nás.");
        }
    }

Blok kódu přístupný v jednu chvíli pouze jednomu vláknu vytvoříme pomocí konstrukce synchronized, která bere jako parametr monitor. Monitorem může být libovolný objekt, my si za tímto účelem vytvoříme jednoduchý atribut. Když bude nyní chtít druhé vlákno vstoupit do kritické (synchronizované) sekce, musí počkat, než ji to první dokončí.

Aplikace nyní funguje jak má a my ji můžeme prohlásit za tzv. ThreadSafe (bezpečnou z hlediska vláken).

Konzolová aplikace
Vybírám 100
na účtu máte ještě 0.
Vybírám 100
na účtu máte ještě 0.
Vybírám 100
na účtu máte ještě 0.
Vybírám 100
na účtu máte ještě 0.
Vybírám 100
na účtu máte ještě 0.
Vybírám 100
na účtu máte ještě 0.
...

Zkusme se ale zamyslet nad tím, zda by kód nešlo nějak optimalizovat. Synchronizovaný blok obaluje všechen kód metody a pro jeden objekt bankomatu existuje právě jeden monitor. No jistě – můžeme využít klíčové slovo synchronized v definici hlavičky metody vyber100(). Jako monitor se pak implicitně využije objekt bankomatu samotný, který nahradí atribut monitor.

Upravme tedy hlavičku metody vyber100 do následující podoby:

private synchronized void vyber100()

Program bude fungovat stejně. Pokud odstraníme atribut monitor, ušetříme tři řádky kódu a za odměnu dostaneme přehlednější kód.

V následujícím kvízu, Kvíz - Tvorba vícevláknových aplikací a synchronizace v Javě, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.


 

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

 

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