====== Synchronizovat nebo označit nestálou proměnnou? ======
Java nabízí několik prostředků synchronizace, když pomineme Java Core API, pak jazyk samotný má v podstatě dvě klíčová slova pro účely synchronizace: **synchonized** a **volatile**. Ale kdy který použít?
===== Synchronized =====
Toto klíčové slovo je známé už každému začínajícímu programátorovi, poskytuje vzájemné vyloučení (mutual exclusivity) v určitém bloku vzhledem k proměnné (mutexu). Typickým příkladem máte například nějaké počítadlo -- takové počítadlo musíte kontrolovat na meze a pokud je vše v pořádku, počítadlo upravit:
if (counter >= Integer.MAX_VALUE)
counter = 0;
Předpokládejme prostředí s více vlákny. Pak tento blok je nutné synchronizovat, to je asi jasné. Mohlo by totiž teoreticky dojít k tomu, že by se počítadlo nastavilo vícekrát na nulu. Použijeme synchronized blok, a to i pro tento případ:
counter++;
Toto už není tak patrné, ale stačí si uvědomit, že překladač jazyka Java //může// vytvořit několik instrukcí -- pro načtení hodnoty, její změnu a uložení z pět do paměti. Ačkoli na některých architekturách lze zajistit atomicitu této operace, v Javě to tak není (a nemůže být).
Je nutné si uvědomit, že synchronizace je nutná také //při čtení// proměnných, JVM totiž může u více vláken optimalizovat přístup do paměti tím, že pro jednotlivá vlákna vytváří kopie proměnných. Jiné vlákno by tedy mohlo "vidět" starý stav proměnné, což odstraní právě synchronized blok, který provede kontrolu a zajistí, aby vlákno přistoupilo k aktuální hodnotě.
Zajímavá otázka však přichází, jakmile se zamyslíme nad tím, vzhledem k čemu máme synchronizovat. Určitě nic nezkazíme, když jako mutex použijeme samotnou instanci třídy, kterou synchronizujeme. Když budeme předpokládat, že každá třída má na starost jeden určitý úkol nebo zapouzdřuje jeden stav, je použití instance ideální.
Výkon programu s mnoha vlákny byste mohli zvýšit použitím //různých mutexů// pro jednotlivé proměnné, ale je nutné si uvědomit, že pravděpodobnost zablokování (deadlock) vlákna by mohla být vyšší, což při použití jednoho mutexu v rámci jedné třídy nehrozí (samozřejmě může ale dojít k deadlocku mezi třídami). Prozkoumání této oblasti není tématem mého zápisku, proto prosím o komentáře, doplnění případně další reakce na toto téma.
===== Volatile =====
Toto klíčové slovo kompilátoru Javy předepisuje, aby si vlákno nedělalo vlastní kopie proměnné. To nám poskytuje možnost vyhnout se **za jisté podmínky** synchronizaci. Tato podmínka je jednoduchá: //hodnota proměnné neovlivňuje stav jiných proměnných, včetně sebe//.
Typickým příkladem je nějaký flag (enabled, disabled, shutdown, started), který se jen nastavuje na určité hodnoty (či instance). Díky klíčovému slovu **volatile** je zajištěno, že všechna vlákna vidí jeden a tentýž stav. Není tedy třeba synchronizace ani při čtení, ani při zápisu do proměnné.
Jakmile ale na stavu proměnné závisí jiná proměnná, nebo proměnná sama, už musíme synchronizovat. Například v tomto případě zajišťuje volatile vláknovou bezpečnost:
public void enable() {
this.enabled = true;
}
Například u počítadla je již synchronizace nutná (závislost proměnné na sobě samé):
public void inc() {
if (this.counter < 100)
this.counter += 5;
}
Toto není nejšťastnější příklad, lepší by byl například u třídy Interval, kde by byly atributy //from// a //to//. Pakliže by byla třída implementovaná "hloupě" a nekontrolovala by, zda atribut //from// je menší než atribut //to//, volatile nám poslouží dobře. Jakmile by ale součástí kontraktu bylo, že třída pohlídá, že se musí jednat o korektní interval, máme zde silnou (dokonce bijektivní) závislost mezi těmito dvěma atributy a volatile pro synchronizaci nebude stačit. Pokud bychom nesynchronizovali, může dojít k vytvoření nekorektního intervalu.
Mezi další příklady použití volatile pro synchronizaci může být proměnná //posledniPrihlasenyUzivatel//, která se vždy jen nastavuje a nebo čte. Žádné další stavy či manipulace s proměnnou, žádný problém. Zajímavou myšlenkou je takzvaný //volatile bean// -- Java Bean, který nemá v přístupových metodách žádnou logiku (a tedy závislosti):
public class User {
private volatile String firstName;
private volatile String lastName;
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() { return lastName; }
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
Tato třída je vláknově bezpečná. Je ale nutné si uvědomit, že u měnitelných tříd a polí klíčové má slovo volatile vliv pouze na referenci, nikoli na datové prvky, které element zapouzdřuje.
===== Kombinace synchronized a volatile =====
Když si dáte pozor, můžete oba přístupy kombinovat. Je tedy možné vytvořit počítadlo, u kterého budete při manipulaci s hodnotou používat synchronized blok a po označení proměnné jako volatile je možné synchronizaci při čtení vypustit. Při psaní podobného kódu doporučuji zvýšenou opatrnost.
===== java.util.concurrent =====
Ve verzi 1.5 jazyka Java byly přidány nové třídy pro konkurenční programování, které vycházejí z myšlenek Douga Leaho (kniha //Concurrent Programming in Java//). Obsahují tři hlavní balíčky:
* java.util.concurrent -- Obsahuje prostředky pro vytváření poolů (Executor, Callable, Future) včetně implementace jednoduchého ThreadPoolu, dále implementace konkurenčních front, map, množin a seznamů (poskytují vyšší výkon při konkurenčním přístupu), podpora pro časování s jemnou granularitou a nové synchronizační prostředky (Semaphore, Exchange a podobně).
* java.util.concurrent.atomic -- Jednoduché proměnné s atomickým přístupem (AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference, AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ...)
* java.util.concurrent.locks -- Sada rozhraní pro lepší práci se zámky: Lock, ReadWriteLock, Condition.
===== Praxe =====
V praxi je úplně nejdůležitější vědět, kdy synchronizaci potřebujeme, a na základě této znalosti třídy v tomto duchu //implementovat// a hlavně //dokumentovat//. Je dobré nejdříve popřemýšlet nad tím, zda by nebylo vhodné třídu navrhnout jako //neměnitelnou// (více v knize J. Blocha: //Effective Java//). Pakliže je třída neměnitelná, stačí to zdokumentovat a správně implementovat -- se synchronizací si od této chvíle pro danou třídu nemusíte dělat starost.
Jakmile však je třída měnitelná, je to "problém". Nejen, že více stavů implikuje více možností, kdy může nastat chyba v běhu programu, ale zejména musíte použít synchronizaci vždy, pokud bude třídu používat více vláken. Opět je nutné se tedy zamyslet, zda je nutné mít třídu //vláknově bezpečnou//, nebo ne. Po rozhodnutí je nezbytné tento fakt opět **zdokumentovat** (někdy se pro tyto účely používají anotace) a v duchu zvoleného přístupu zajistit korektní implementaci.
Pozor u kolekcí v Javě, některé jsou vláknově bezpečné, jiné ne -- je nutné používat dokumentaci k API a některé třídy obalovat synchro-wrappery. Tato problematika má mnoho aspektů, nemohu zacházet do detailů, protože tématem zápisku je synchronized vs volatile. Nakonec bych si ještě dovolil jednu poznámku -- pokud pracuje na projektu více lidí, je nutné vždy před úpravou "cizí" třídy (třídy, kterou jste nepsali vy) kouknout do komentáře. Mohli byste totiž nevědomky předělat neměnitelnou třídu na měnitelnou, případně vláknově bezpečnou na vláknově //nebezpečnou//.
{{tag>java}}
~~DISCUSSION~~