Programování

Java 101: Porozumění podprocesům Java, Část 3: Plánování podprocesů a čekání / upozornění

Tento měsíc pokračuji ve čtyřdílném úvodu do podprocesů Java zaměřením na plánování podprocesů, mechanismus čekání / upozornění a přerušení podprocesů. Budete zkoumat, jak buď JVM, nebo plánovač podprocesů operačního systému vybere další podproces pro provedení. Jak zjistíte, pro výběr plánovače vláken je důležitá priorita. Prozkoumáte, jak vlákno čeká, dokud nepřijme oznámení z jiného vlákna, než bude pokračovat v provádění, a naučíte se, jak používat mechanismus čekání / upozornění pro koordinaci provádění dvou vláken ve vztahu producent-spotřebitel. Nakonec se naučíte, jak předčasně probudit spící nebo čekající vlákno na ukončení vlákna nebo jiné úkoly. Také vás naučím, jak vlákno, které ani nespí, ani čeká, detekuje požadavek na přerušení z jiného vlákna.

Tento článek (součást archivů JavaWorld) byl v květnu 2013 aktualizován o nové výpisy kódů a zdrojový kód ke stažení.

Porozumění vláknům Java - přečtěte si celou sérii

  • Část 1: Představujeme vlákna a spouštěcí tabulky
  • Část 2: Synchronizace
  • Část 3: Plánování vlákna, čekání / upozornění a přerušení vlákna
  • Část 4: Skupiny vláken, volatilita, místní proměnné vlákna, časovače a smrt vlákna

Plánování vláken

V idealizovaném světě by všechna podprocesy programů měly své vlastní procesory, na kterých by mohly běžet. Dokud nenastane čas, kdy počítače mají tisíce nebo miliony procesorů, vlákna musí často sdílet jeden nebo více procesorů. JVM nebo operační systém základní platformy dešifruje, jak sdílet prostředek procesoru mezi vlákny - úkol známý jako plánování vláken. Ta část JVM nebo operačního systému, která provádí plánování vláken, je plánovač vláken.

Poznámka: Pro zjednodušení diskuse o plánování podprocesů se zaměřuji na plánování podprocesů v kontextu jednoho procesoru. Tuto diskusi můžete extrapolovat na více procesorů; Ten úkol nechávám na vás.

Nezapomeňte na dva důležité body týkající se plánování podprocesů:

  1. Java nenutí virtuální počítač naplánovat vlákna konkrétním způsobem nebo obsahovat plánovač vláken. To znamená plánování podprocesů závislých na platformě. Při psaní programu Java, jehož chování závisí na tom, jak jsou vlákna naplánována, a proto musí fungovat konzistentně napříč různými platformami, musíte postupovat opatrně.
  2. Naštěstí při psaní programů Java musíte myslet na to, jak Java naplánuje vlákna pouze tehdy, když alespoň jedno z vláken vašeho programu silně používá procesor po dlouhou dobu a mezilehlé výsledky provádění tohoto vlákna se ukáží jako důležité. Například applet obsahuje vlákno, které dynamicky vytváří obraz. Pravidelně chcete, aby malířské vlákno nakreslilo aktuální obsah daného obrázku, aby uživatel mohl vidět, jak obrázek postupuje. Aby bylo zajištěno, že podproces výpočtu nebude monopolizovat procesor, zvažte plánování podprocesů.

Prozkoumejte program, který vytváří dvě podprocesy náročné na procesor:

Výpis 1. SchedDemo.java

// SchedDemo.java třída SchedDemo {public static void main (String [] args) {new CalcThread ("CalcThread A"). Start (); nový CalcThread ("CalcThread B"). start (); }} třída CalcThread rozšiřuje vlákno {CalcThread (název řetězce) {// předává název vrstvě vlákna. super (jméno); } double calcPI () {boolean negative = true; dvojité pi = 0,0; for (int i = 3; i <100000; i + = 2) {if (negative) pi - = (1,0 / i); else pi + = (1,0 / i); negativní =! negativní; } pi + = 1,0; pi * = 4,0; návrat pi; } public void run () {for (int i = 0; i <5; i ++) System.out.println (getName () + ":" + calcPI ()); }}

SchedDemo vytvoří dvě vlákna, z nichž každé vypočítá hodnotu pí (pětkrát) a vytiskne každý výsledek. V závislosti na tom, jak vaše implementace JVM naplánuje vlákna, se může zobrazit výstup podobný následujícímu:

CalcThread A: 3,1415726535897894 CalcThread B: 3,1415726535897894 CalcThread A: 3,1415726535897894 CalcThread A: 3,1415726535897894 CalcThread B: 3,1415726535897894 CalcThread A: 3,1415726535897894 CalcThread A: 3,1415726535897894 CalcThread B: 3,1415726535897894 CalcThread B: 3,1415726535897894 CalcThread B: 3,1415726535897894

Podle výše uvedeného výstupu plánovač vláken sdílí procesor mezi oběma vlákny. Mohli byste však vidět výstup podobný tomuto:

CalcThread A: 3,1415726535897894 CalcThread A: 3,1415726535897894 CalcThread A: 3,1415726535897894 CalcThread A: 3,1415726535897894 CalcThread A: 3,1415726535897894 CalcThread B: 3,1415726535897894 CalcThread B: 3,1415726535897894 CalcThread B: 3,1415726535897894 CalcThread B: 3,1415726535897894 CalcThread B: 3,1415726535897894

Výše uvedený výstup ukazuje, že plánovač podprocesů upřednostňuje jedno vlákno před druhým. Dva výstupy výše ilustrují dvě obecné kategorie plánovačů vláken: zelený a nativní. V dalších částech prozkoumám jejich rozdíly v chování. Při diskusi o každé kategorii odkazuji na stavy vláken, z toho jsou čtyři:

  1. Počáteční stav: Program vytvořil objekt vlákna vlákna, ale vlákno ještě neexistuje, protože je objekt vlákna Start() metoda dosud nebyla volána.
  2. Spustitelný stav: Toto je výchozí stav vlákna. Po volání na Start() dokončí, vlákno se stane spustitelným bez ohledu na to, zda je vlákno spuštěno, tj. pomocí procesoru. Ačkoli mnoho vláken může být spustitelných, aktuálně běží pouze jeden. Plánovače vláken určují, které spustitelné vlákno se přiřadí procesoru.
  3. Blokovaný stav: Když vlákno provede spát(), Počkejte()nebo připojit() metody, když se vlákno pokusí načíst data, která dosud nejsou k dispozici v síti, a když vlákno čeká na získání zámku, je toto vlákno v zablokovaném stavu: není ani spuštěno, ani není v poloze ke spuštění. (Pravděpodobně si můžete vzpomenout na jiné časy, kdy by vlákno počkalo, až se něco stane.) Když se blokované vlákno odblokuje, toto vlákno se přesune do spustitelného stavu.
  4. Stav ukončení: Jakmile poprava zanechá nit běh() metoda, toto vlákno je ve stavu ukončení. Jinými slovy vlákno přestane existovat.

Jak plánovač vláken vybírá, které spustitelné vlákno se má spustit? Začínám odpovídat na tuto otázku, když diskutuji o plánování zelených vláken. Dokončuji odpověď při diskusi o nativním plánování vlákna.

Plánování zelených vláken

Ne všechny operační systémy, například starodávný systém Microsoft Windows 3.1, podporují vlákna. Pro takové systémy může Sun Microsystems navrhnout JVM, který rozděluje jeho jediný podproces provádění na více podprocesů. JVM (nikoli operační systém podkladové platformy) dodává logiku vláken a obsahuje plánovač vláken. Vlákna JVM jsou zelené nitě, nebo uživatelská vlákna.

Plánovač vláken JVM naplánuje zelená vlákna podle přednost—Relativní důležitost vlákna, kterou vyjadřujete jako celé číslo z dobře definované oblasti hodnot. Plánovač vláken JVM obvykle vybírá vlákno s nejvyšší prioritou a umožňuje tomuto vláknu běžet, dokud nebude buď ukončeno, nebo blokováno. V té době plánovač vláken vybere vlákno s další nejvyšší prioritou. Toto vlákno (obvykle) běží, dokud se neukončí nebo neblokuje. Pokud se během běhu vlákna odblokuje vlákno s vyšší prioritou (možná vypršela doba spánku vlákna s vyšší prioritou), plánovač vlákna preempts, nebo přeruší vlákno s nižší prioritou a přiřadí odblokované vlákno s vyšší prioritou procesoru.

Poznámka: Spustitelné vlákno s nejvyšší prioritou se ne vždy spustí. Tady je Specifikace jazyka Java 'převzít prioritu:

Každé vlákno má a přednost. Pokud existuje konkurence pro zpracování zdrojů, vlákna s vyšší prioritou se obecně provádějí přednostně před vlákny s nižší prioritou. Taková preference však není zárukou, že vlákno s nejvyšší prioritou bude vždy spuštěno, a priority vlákna nelze použít ke spolehlivé implementaci vzájemného vyloučení.

Toto přiznání hodně napovídá o implementaci zelených vláken JVM. Tyto JVM si nemohou dovolit nechat vlákna blokovat, protože by to spojilo jediné vlákno provádění JVM. Proto když vlákno musí blokovat, například když toto vlákno načítá data pomalu, aby dorazilo ze souboru, může JVM zastavit provádění vlákna a použít mechanismus dotazování k určení, kdy přijdou data. Zatímco vlákno zůstává zastaveno, plánovač vláken JVM může naplánovat spuštění vlákna s nižší prioritou. Předpokládejme, že data dorazí, zatímco je spuštěno vlákno s nižší prioritou. Ačkoli vlákno s vyšší prioritou by se mělo spustit, jakmile dorazí data, nedojde k tomu, dokud JVM dále nevyzve operační systém a neobjeví příjezd. Proto vlákno s nižší prioritou běží, i když by vlákno s vyšší prioritou mělo běžet. o této situaci si musíte dělat starosti, pouze když potřebujete chování Java v reálném čase. Ale pak Java není operační systém v reálném čase, tak proč si dělat starosti?

Chcete-li pochopit, které spustitelné zelené vlákno se stane aktuálně spuštěným zeleným vláknem, zvažte následující. Předpokládejme, že se vaše aplikace skládá ze tří podprocesů: hlavního podprocesu, který spouští hlavní() metoda, podproces výpočtu a podproces, který čte vstup z klávesnice. Pokud není žádný vstup z klávesnice, čtecí vlákno se zablokuje. Předpokládejme, že podproces pro čtení má nejvyšší prioritu a podproces pro výpočet má nejnižší prioritu. (Pro zjednodušení také předpokládejme, že nejsou k dispozici žádná další interní vlákna JVM.) Obrázek 1 ilustruje provedení těchto tří vláken.

V čase T0 se spustí hlavní vlákno. V čase T1 spustí hlavní vlákno podproces výpočtu. Protože vlákno pro výpočet má nižší prioritu než hlavní vlákno, vlákno pro výpočet čeká na procesor. V čase T2 začne hlavní vlákno čtecí vlákno. Protože vlákno pro čtení má vyšší prioritu než hlavní vlákno, čeká hlavní vlákno na procesor, zatímco běží vlákno pro čtení. V čase T3 se čtecí vlákno blokuje a běží hlavní vlákno. V čase T4 se čtecí vlákno odblokuje a běží; hlavní vlákno čeká. Nakonec v době T5 běží čtecí vlákno a běží hlavní vlákno. Toto střídání v provádění mezi čtením a hlavními vlákny pokračuje, dokud běží program. Vlákno pro výpočet se nikdy nespustí, protože má nejnižší prioritu, a proto hladuje pozornost procesoru, což je situace známá jako hladovění procesoru.

Tento scénář můžeme změnit tak, že vláknu výpočtu dáme stejnou prioritu jako hlavní vlákno. Obrázek 2 ukazuje výsledek počínaje časem T2. (Před T2 je obrázek 2 totožný s obrázkem 1.)

V čase T2 běží čtecí vlákno, zatímco hlavní a výpočtové vlákno čekají na procesor. V čase T3 se čtecí vlákno blokuje a vlákno pro výpočet běží, protože hlavní vlákno běželo těsně před čtecím vláknem. V čase T4 se čtecí vlákno odblokuje a běží; hlavní a výpočetní vlákna čekají. V čase T5 se čtecí vlákno blokuje a hlavní vlákno běží, protože vlákno pro výpočet běželo těsně před čtecím vláknem. Toto střídání v provádění mezi hlavním vláknem a vlákny výpočtu pokračuje tak dlouho, dokud program běží, a závisí na běhu a blokování vlákna s vyšší prioritou.

Musíme zvážit jednu poslední položku v plánování zelených vláken. Co se stane, když vlákno s nižší prioritou obsahuje zámek, který vlákno s vyšší prioritou vyžaduje? Vlákno s vyšší prioritou blokuje, protože nemůže získat zámek, což znamená, že vlákno s vyšší prioritou má skutečně stejnou prioritu jako vlákno s nižší prioritou. Například vlákno priority 6 se pokusí získat zámek, který vlákno priority 3 obsahuje. Protože vlákno priority 6 musí počkat, dokud nezíská zámek, vlákno priority 6 skončí s prioritou 3 - jev známý jako prioritní inverze.

Prioritní inverze může značně zpozdit provedení vlákna s vyšší prioritou. Předpokládejme například, že máte tři vlákna s prioritami 3, 4 a 9. Je spuštěno vlákno priority 3 a ostatní vlákna jsou blokována. Předpokládejme, že vlákno s prioritou 3 popadne zámek a vlákno s prioritou 4 se odblokuje. Vlákno priority 4 se stane aktuálně spuštěným vláknem. Protože vlákno s prioritou 9 vyžaduje zámek, pokračuje v čekání, dokud vlákno s prioritou 3 zámek neuvolní. Vlákno s prioritou 3 však nemůže uvolnit zámek, dokud vlákno s prioritou 4 neblokuje nebo nekončí. Výsledkem je, že podproces priority 9 zpozdí jeho provedení.