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é 136x (2.07 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java