IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

3. diel - Multithreading v Jave - Synchronizácia v praxi

V minulej lekcii, Multithreading v Jave - Daemon, join a synchronized , sme načali tému synchronizácie. Dnes sa pozrieme na využitie synchronizácie v praxi.

Prepínanie kontextu

O prepínanie kontexte sme už hovorili v prvom diele. Ide o spôsob, akým procesor prepína medzi jednotlivými vláknami bežiacimi na jednom procesorové jadrá. A práve kvôli tomuto prepínanie sa musíme starať o synchronizáciu. Ako som už spomínal, nikdy totiž nemôžeme presne vedieť, kedy k prepnutiu dôjde. Poďme si to demonštrovať na príklade:

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();
    }
}

Vytvorili sme triedu Prepinac s tromi metódami:

Prvé 2 metódy triedy sú veľmi jednoduché a simulujú nejakú dlhšiu činnosť. Prvá metóda do nekonečna vypisuje nuly do konzoly, druhá metóda rovnakým spôsobom vypisuje jedničky.

Metóda prepinej() je pre nás už zaujímavejšie. Sama spustí výpis jedničiek, ale ešte predtým vytvorí nové vlákno, ktorému priradí metódu pre výpis núl. Toto vlákno potom tiež spustí.

Metódu main si poupraví, aby vytvorila objekt prepínačov a zavolala na ňom metódu prepinej().

Keď teraz program spustíme, pravdepodobne sa vám nič nevypíše. Celý výpis sa zobrazí až po ukončení programu. Prečo? Jednoducho preto, že konzola prichádzajúce znaky nevypisuje, ale napcháva ich do vyrovnávacej pamäte, ktorú vypíše až po ukončení riadku. Avšak skôr či neskôr sa náš binárne koktail ukáže. U mňa vyzeral takto:

Konzolová aplikácia
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 samozrejme bude vyzerať pri každom spustení inak.

Všimnite si, že jednotky a nuly nie sú úplne na striedačku, ako by sme mohli očakávať. Je vidieť, ako vlákno chvíľu beží a potom ich uspanie. Intervaly sa tiež líšia, aj keď priemerne beží obe vlákna rovnako dlho. Myslím, že nemusím pripomínať, že je to dané prepínaním kontextu. Poďme sa ale zamyslieť nad tým, ako takéto prepínanie môže ovplyvniť reálnu aplikáciu.

Naprogramujeme si triedu reprezentujúci bankomat, ktorý eviduje nejakú hotovosť.

ThreadSafety

Vlákno môže pristupovať k inštančným alebo statickým premenným. Práve toto je jeden zo spôsobov, ktorými spolu môžu jednotlivé vlákna komunikovať. Ako už tušíme, háčik bude v už spomínanej synchronizáciu. Predstavme si nasledujúce triedu:

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.");
    }

}

Trieda reprezentuje bankomat, ktorý eviduje nejakú hotovosť. Tá je pri vytvorení bankomatu 100 Sk. Ďalej disponuje jednoduchou metódou vyber100(), ktorá vyberie 100 korún v prípade, že je na účte potrebný zostatok. Zaujímavá je pre nás metóda vyberVlakny(), ktorá sa pomocou 2 vlákien (aktuálneho a novovytvoreného) pokúsi vybrať 100 Sk. Ak sa s hotovosťou náhodou dostaneme do mínusu, vypíšeme o tom hlásenie.

V kóde som pre vytvorenie objektu typu Runnable použil odkaz na inštančný metódu vyber100() pomocou operátora ::. To je novinka Javy 8 a tých, ktorí nevedia o čo sa jedná, znovu odkázať na článok o zmenách uskutočnených novou verziou Javy.

Do hlavnej metódy pridáme kód, ktorý vykoná 200 výberov na 100 bankomatoch:

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

A aplikáciu spustíme:

Konzolová aplikácia
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 niečo nesedí. Kde je problém?

V metóde vyber100() kontrolujeme podmienkou, či je na účte dostatočná hotovosť. Predstavte si, že je na účte 100 Sk. Podmienka teda platí a systém vlákno uspí treba ihneď za vyhodnotením podmienky. Toto vlákno teda čaká. Druhé vlákno tiež skontroluje podmienku, ktorá platí, a odpočíta 100 Sk. Potom sa prebudí prvé vlákno, ktoré je už za podmienkou a tiež odpočíta 100 Sk. Vo výsledku máme na účte teda záporný zostatok!

Vidíme, že práca s vláknami prináša nová úskalia, s ktorými sme sa doteraz ešte nestretli. Situáciu vyriešime pomocou synchronizácie.

Synchronizácia

Iste sa zhodneme na tom, že sekcia s overením zostatku a jeho následnú zmenou musí prebehnúť vždy celá, inak sa dostávame do vyššie uvedenej situácie. Problém vyriešime tým, že sekciu, kde sa zdieľanou premennou hotovosť pracujeme, obalíme blokom synchronized. Kód triedy bankomatu upravíme do nasledujúcej 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 prístupný v jednej chvíli len jednému vláknu vytvoríme pomocou konštrukcie synchronized, ktorá berie ako parameter monitor. Monitorom môže byť ľubovoľný objekt, my si za týmto účelom vytvoríme jednoduchý atribút. Keď bude teraz chcieť druhé vlákno vstúpiť do kritickej (synchronizované) sekcia, musí počkať, než ju to prvý dokončí.

Aplikácia teraz funguje ako má a my ju môžeme vyhlásiť za tzv. ThreadSafe (bezpečnú z hľadiska vlákien).

Konzolová aplikácia
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.
...

Skúsme sa ale zamyslieť nad tým, či by kód nešlo nejako optimalizovať. Synchronizovaný blok obaľuje všetok kód metódy a pre jeden objekt bankomatu existuje práve jeden monitor. No iste - môžeme využiť kľúčové slovo synchronized v definícii hlavičky metódy vyber100(). Ako monitor sa potom implicitne využije objekt bankomatu samotný, ktorý nahradí atribút monitor.

Upravme teda hlavičku metódy vyber100 do nasledujúcej podoby:

private synchronized void vyber100()

Program bude fungovať rovnako. Ak odstránime atribút monitor, ušetríme tri riadky kódu a za odmenu dostaneme prehľadnejšie kód.

Nabudúce, v lekcii Multithreading v Jave - Mezivláknová komunikácia , sa zameriame na mezivláknovou komunikáciu.


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 137x (2.07 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java

 

Predchádzajúci článok
Multithreading v Jave - Daemon, join a synchronized
Všetky články v sekcii
Viacvláknové aplikácie v Jave
Preskočiť článok
(neodporúčame)
Multithreading v Jave - Mezivláknová komunikácia
Článok pre vás napísal Matěj Kripner
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Student, programátor v CZ.NIC.
Aktivity