Programovací jazyk Java a JVM ( Java Virtual Machine ) jsou navrženy tak, aby podporovaly paralelní výpočty a všechny výpočty se provádějí v kontextu vlákna . Více vláken může sdílet objekty a prostředky; každé vlákno vykonává své vlastní instrukce (kód), ale může potenciálně přistupovat k libovolnému objektu v programu. Za koordinaci (nebo „ synchronizaci “) vláken během operací čtení a zápisu na sdílených objektech je odpovědný programátor . Synchronizace vláken je potřebná, aby bylo zajištěno, že pouze jedno vlákno může přistupovat k objektu současně, a aby se zabránilo vláknům v přístupu k neúplně aktualizovaným objektům, zatímco na nich jiné vlákno pracuje. Jazyk Java má vestavěné konstrukce podpory synchronizace vláken.
Většina implementací Java Virtual Machine používá ke spuštění programu jediný proces a v programovacím jazyce Java je paralelní výpočet nejčastěji spojován s vlákny . Nitě se někdy nazývají světelné procesy .
Vlákna mezi sebou sdílejí procesní prostředky, jako je paměť a otevřené soubory. Tento přístup vede k efektivní, ale potenciálně problematické komunikaci. Každá aplikace má alespoň jedno běžící vlákno. Vlákno, od kterého začíná provádění programu, se nazývá hlavní nebo hlavní . Hlavní vlákno je schopno vytvářet další vlákna ve formě objektů Runnablenebo Callable. (Rozhraní Callableje podobné v Runnabletom, že obě jsou navrženy pro třídy, které budou vytvořeny v samostatném vláknu. Runnable, však nevrací výsledek a nemůže vyvolat zaškrtnutou výjimku .)
Každé vlákno lze naplánovat tak, aby běželo na samostatném jádře CPU, použít časové dělení na jednom jádře procesoru nebo použít časové dělení na více procesorech. V posledních dvou případech bude systém pravidelně přepínat mezi vlákny a střídavě umožňuje spuštění jednoho nebo druhého vlákna. Toto schéma se nazývá pseudoparalelismus. Neexistuje žádné univerzální řešení, které by přesně říkalo, jak budou vlákna Java převedena na vlákna nativní OS. Záleží na konkrétní implementaci JVM.
V Javě je vlákno reprezentováno jako podřízený objekt Thread. Tato třída zapouzdřuje standardní závitové mechanismy. Vlákna lze spravovat buď přímo, nebo prostřednictvím abstraktních mechanismů, jako je Executor a kolekce z balíčku java.util.concurrent.
Spuštění vláknaExistují dva způsoby, jak zahájit nové vlákno:
Přerušení je znamení pro vlákno, že by mělo zastavit aktuální práci a udělat něco jiného. Vlákno může poslat přerušení voláním metody interrupt() objektu , Threadpokud potřebuje přerušit přidružené vlákno. Mechanismus přerušení je implementován pomocí interního příznaku stavu přerušení (příznaku přerušení) třídy Thread. Voláním Thread.interrupt() se tento příznak zvýší. Podle konvence jakákoli metoda, která končí InterruptedException , resetuje příznak přerušení. Existují dva způsoby, jak zkontrolovat, zda je tento příznak nastaven. Prvním způsobem je zavolat metodu bool isInterrupted() objektu vlákna, druhým způsobem je zavolat statickou metodu bool Thread.interrupted() . První metoda vrátí stav příznaku přerušení a ponechá tento příznak nedotčený. Druhá metoda vrátí stav příznaku a resetuje jej. Všimněte si, že Thread.interrupted() je statická metoda třídy Threada jejím voláním se vrací hodnota příznaku přerušení vlákna, ze kterého byla volána.
Čekání na dokončeníJava poskytuje mechanismus, který umožňuje jednomu vláknu čekat na dokončení jiného vlákna. K tomu se používá metoda Thread.join() .
DémoniV Javě se proces ukončí, když skončí jeho poslední vlákno. I když je metoda main() již dokončena, ale vlákna, která vytvořila, stále běží, systém počká na jejich dokončení. Toto pravidlo však neplatí pro speciální druh vlákna – démony. Pokud poslední normální vlákno procesu skončilo a zbývají pouze vlákna démona, budou násilně ukončena a proces skončí. Nejčastěji se vlákna démona používají k provádění úloh na pozadí, které obsluhují proces během jeho životnosti.
Deklarování vlákna jako démona je docela jednoduché – před spuštěním vlákna musíte zavolat jeho metodu setDaemon(true) ; Zda je vlákno démonem, můžete zkontrolovat zavoláním jeho booleovské metody isDaemon() .
VýjimkyVyvolaná a neošetřená výjimka způsobí ukončení vlákna. Hlavní vlákno automaticky vytiskne výjimku do konzole a uživatelsky vytvořená vlákna tak mohou učinit pouze registrací obsluhy. [1] [2]
Paměťový model Java [1] popisuje interakci vláken prostřednictvím paměti v programovacím jazyce Java. Na moderních počítačích se kód často kvůli rychlosti nespouští v pořadí, ve kterém je napsán. Permutace je provedena kompilátorem , procesorem a paměťovým subsystémem . Programovací jazyk Java nezaručuje atomičnost operací a sekvenční konzistenci při čtení nebo zápisu polí sdílených objektů. Toto řešení uvolňuje ruce kompilátoru a umožňuje optimalizace (jako je alokace registrů , odstranění společných podvýrazů a odstranění nadbytečných operací čtení ) na základě permutace operací přístupu do paměti. [3]
Vlákna komunikují sdílením přístupu k polím a objektům, na které pole odkazují. Tato forma komunikace je extrémně efektivní, ale umožňuje dva druhy chyb: rušení vláken a chyby konzistence paměti. Aby se zabránilo jejich výskytu, existuje synchronizační mechanismus.
Přeuspořádání (přeuspořádání, přeuspořádání) se projevuje v nesprávně synchronizovaných vícevláknových programech, kde jedno vlákno může pozorovat efekty produkované jinými vlákny a takové programy mohou být schopny detekovat, že aktualizované hodnoty proměnných se stanou viditelnými pro jiná vlákna v jiném vlákně. pořadí, než je uvedeno ve zdrojovém kódu.
K synchronizaci vláken v Javě se používají monitory , což je mechanismus na vysoké úrovni, který umožňuje pouze jednomu vláknu najednou spustit blok kódu chráněný monitorem. Chování monitorů je zvažováno z hlediska zámků ; Každý objekt má přiřazen jeden zámek.
Synchronizace má několik aspektů. Nejlépe pochopitelné je vzájemné vyloučení - pouze jedno vlákno může vlastnit monitor, takže synchronizace na monitoru znamená, že jakmile jedno vlákno vstoupí do synchronizovaného bloku chráněného monitorem, žádné další vlákno nemůže vstoupit do bloku chráněného tímto monitorem, dokud první vlákno opustí synchronizovaný blok.
Ale synchronizace je víc než jen vzájemné vyloučení. Synchronizace zajišťuje, že data zapsaná do paměti před nebo v rámci synchronizovaného bloku budou viditelná pro ostatní vlákna, která jsou synchronizována na stejném monitoru. Poté, co opustíme synchronizovaný blok, uvolníme monitor, což má za následek vyprázdnění mezipaměti do hlavní paměti, takže zápisy provedené naším vláknem mohou být viditelné pro ostatní vlákna. Než můžeme vstoupit do synchronizovaného bloku, získáme monitor, což způsobí zneplatnění místní mezipaměti procesoru, takže proměnné jsou načteny z hlavní paměti. Poté můžeme vidět všechny záznamy, které byly viditelné po předchozím vydání monitoru. (JSR 133)
Čtení a zápis na pole je atomická operace , pokud je pole buď prohlášeno za nestálé nebo chráněno jedinečným zámkem získaným před jakýmkoli čtením a zápisem.
Zámky a synchronizované blokyEfektu vzájemného vyloučení a synchronizace vláken je dosaženo zadáním synchronizovaného bloku nebo metody, která získává zámek implicitně, nebo explicitním získáním zámku (například ReentrantLockz balíčku java.util.concurrent.locks). Oba přístupy mají stejný vliv na chování paměti. Pokud jsou všechny pokusy o přístup k určitému poli chráněny stejným zámkem, jsou operace čtení a zápisu tohoto pole atomické .
Nestálá polePři použití na pole klíčové slovo volatilezaručuje:
Volatile-pole jsou atomová. Čtení z volatile-pole má stejný účinek jako získání zámku: data v pracovní paměti jsou prohlášena za neplatná a hodnota volatile-pole je znovu načtena z paměti. Zápis do volatile-pole má na paměť stejný účinek jako uvolnění zámku: volatile-pole je okamžitě zapsáno do paměti.
Závěrečná polePole, které je deklarováno jako konečné, se nazývá konečné a nelze jej po inicializaci změnit. Poslední pole objektu jsou inicializována v jeho konstruktoru. Pokud se konstruktor řídí určitými jednoduchými pravidly, pak správná hodnota konečného pole bude viditelná pro ostatní vlákna bez synchronizace. Jednoduchým pravidlem je, že tento odkaz nesmí opustit konstruktor, dokud není dokončen.
Počínaje verzí JDK 1.2 obsahuje Java standardní sadu tříd kolekce Java Collections Framework .
Doug Lee , který také přispěl k implementaci Java Collections Framework, vyvinul souběžný balíček , který obsahuje několik synchronizačních primitiv a velký počet tříd souvisejících s kolekcemi. [5] Práce na něm pokračovaly jako součást JSR 166 [6] pod předsednictvím Douga Lee .
Vydání JDK 5.0 zahrnovalo mnoho dodatků a objasnění k modelu souběžnosti Java. Poprvé byla souběžná API vyvinutá JSR 166 zahrnuta do JDK. JSR 133 poskytoval podporu pro dobře definované atomické operace ve vícevláknovém/multiprocesorovém prostředí.
Java SE 6 i Java SE 7 přinášejí změny a doplnění JSR 166 API.