Programování

Java 101: Souběžnost prostředí Java bez bolesti, část 1

S narůstající složitostí souběžných aplikací mnoho vývojářů zjistilo, že nízkoúrovňové podprocesy Java nejsou pro jejich programovací potřeby dostatečné. V takovém případě může být čas objevit Java Concurrency Utilities. Začněte s java.util.concurrents podrobným úvodem Jeffa Friesena do rámce Executor, typů synchronizátorů a balíčku Java Concurrent Collections.

Java 101: Nová generace

První článek v této nové sérii JavaWorld představuje Java Date and Time API.

Platforma Java poskytuje funkce podprocesů na nízké úrovni, které vývojářům umožňují psát souběžné aplikace, kde se současně spouštějí různá podprocesy. Standardní podprocesy Java však mají některé nevýhody:

  • Nízkoúrovňové primitivy souběžnosti Java (synchronizované, nestálý, Počkejte(), oznámit(), a notifyAll ()) nejsou snadno použitelné správně. Nebezpečí detekce závitu, jako je uváznutí, hladovění podprocesů a rasové podmínky, které vyplývají z nesprávného použití primitiv, je také těžké odhalit a ladit.
  • Spoléhat se na synchronizované koordinovat přístup mezi vlákny vede k problémům s výkonem, které ovlivňují škálovatelnost aplikací, což je požadavek mnoha moderních aplikací.
  • Základní schopnosti vláken Java jsou také nízká úroveň. Vývojáři často potřebují konstrukty na vyšší úrovni, jako jsou semafory a fondy vláken, které nízkoúrovňové podprocesy Java nenabízejí. Výsledkem je, že vývojáři vytvoří své vlastní konstrukce, které jsou časově náročné a náchylné k chybám.

Rámec JSR 166: Concurrency Utilities byl navržen tak, aby splňoval potřebu podprocesů na vysoké úrovni. Rámec, který byl zahájen počátkem roku 2002, byl formován a implementován o dva roky později v prostředí Java 5. Vylepšení následovala v prostředí Java 6, Java 7 a nadcházející Java 8.

Tato dvoudílná Java 101: Nová generace série představuje vývojářům softwaru, kteří jsou obeznámeni se základními podprocesy Java, balíčky a rámec Java Concurrency Utilities. V části 1 představuji přehled rámce Java Concurrency Utilities a představím jeho rámec Executor, synchronizační nástroje a balíček Java Concurrent Collections.

Porozumění podprocesům Java

Než se ponoříte do této série, ujistěte se, že jste obeznámeni se základy závitování. Začněte s Java 101 úvod do nízkoúrovňových podprocesů Java:

  • Část 1: Představujeme vlákna a spouštěcí tabulky
  • Část 2: Synchronizace vláken
  • Čá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

Uvnitř Java Concurrency Utilities

Rámec Java Concurrency Utilities je knihovna typy které jsou určeny k použití jako stavební bloky pro vytváření souběžných tříd nebo aplikací. Tyto typy jsou bezpečné pro vlákna, byly důkladně otestovány a nabízejí vysoký výkon.

Typy v Java Concurrency Utilities jsou organizovány do malých rámců; jmenovitě rámec Exekutora, synchronizátor, souběžné kolekce, zámky, atomové proměnné a Fork / Join. Jsou dále organizovány do hlavního balíčku a dvojice dílčích balíčků:

  • java.util.concurrent obsahuje typy obslužných programů na vysoké úrovni, které se běžně používají při souběžném programování. Mezi příklady patří semafory, bariéry, oblasti vláken a souběžné hashmapy.
    • The java.util.concurrent.atomic dílčí balíček obsahuje nízkoúrovňové třídy obslužných programů, které podporují bezzámkové programování bezpečné pro vlákna na jednotlivých proměnných.
    • The java.util.concurrent.locks dílčí balíček obsahuje nízkoúrovňové typy nástrojů pro zamykání a čekání na podmínky, které se liší od použití nízkoúrovňové synchronizace a monitorů Java.

Rámec Java Concurrency Utilities také odhaluje nízkou úroveň porovnat a vyměnit (CAS) hardwarová instrukce, jejíž varianty jsou běžně podporovány moderními procesory. CAS je mnohem lehčí než synchronizační mechanismus založený na monitoru Java a používá se k implementaci některých vysoce škálovatelných souběžných tříd. Na základě CAS java.util.concurrent.locks.ReentrantLock Například třída je výkonnější než ekvivalentní monitor synchronizované primitivní. Znovu zadejte zámek nabízí větší kontrolu nad zamykáním. (V části 2 vysvětlím více o tom, jak CAS funguje java.util.concurrent.)

System.nanoTime ()

Rámec Java Concurrency Utilities zahrnuje dlouhý nanoTime (), který je členem java.lang.System třída. Tato metoda umožňuje přístup ke zdroji času nanosekundové zrnitosti pro provádění měření relativního času.

V následujících částech představím tři užitečné funkce Java Concurrency Utilities, nejprve vysvětlím, proč jsou pro moderní souběžnost tak důležité, a poté předvedu, jak fungují pro zvýšení rychlosti, spolehlivosti, efektivity a škálovatelnosti souběžných aplikací Java.

Rámec exekutora

V závitech, a úkol je jednotka práce. Jedním z problémů nízkoúrovňových podprocesů v Javě je to, že zadávání úkolů je úzce spojeno se zásadami provádění úkolů, jak ukazuje seznam 1.

Výpis 1. Server.java (verze 1)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; třída Server {public static void main (String [] args) hodí IOException {ServerSocket socket = nový ServerSocket (9000); while (true) {final Socket s = socket.accept (); Runnable r = new Runnable () {@Override public void run () {doWork (s); }}; new Thread (r) .start (); }} static void doWork (Socket s) {}}

Výše uvedený kód popisuje jednoduchou serverovou aplikaci (s doWork (Socket) ponecháno prázdné pro stručnost). Vlákno serveru opakovaně volá socket.accept () čekat na příchozí požadavek a poté, co přijde, spustí podproces, který bude tento požadavek obsluhovat.

Protože tato aplikace vytváří nové vlákno pro každý požadavek, nemění se dobře, když čelí velkému počtu požadavků. Například každý vytvořený podproces vyžaduje paměť a příliš mnoho podprocesů může vyčerpat dostupnou paměť a vynutit ukončení aplikace.

Tento problém můžete vyřešit změnou politiky provádění úloh. Spíše než vždy vytvářet nové vlákno, můžete použít fond vláken, ve kterém by pevný počet vláken obsluhoval příchozí úkoly. Chcete-li provést tuto změnu, museli byste aplikaci přepsat.

java.util.concurrent zahrnuje rámec Executor, malý rámec typů, které oddělují odesílání úkolů od zásad provádění úkolů. Pomocí rámce Executor je možné snadno vyladit zásady provádění úloh programu, aniž byste museli výrazně přepisovat svůj kód.

Uvnitř rámce exekutora

Rámec Exekutora je založen na Vykonavatel rozhraní, které popisuje vykonavatel jako jakýkoli objekt schopný vykonat java.lang.Runnable úkoly. Toto rozhraní deklaruje následující osamělou metodu pro provádění a Spustitelný úkol:

void execute (příkaz Runnable)

Odesíláte a Spustitelný úkol předáním spustit (Runnable). Pokud exekutor nemůže z nějakého důvodu provést úkol (například pokud byl exekutor vypnut), tato metoda vyvolá RejectedExecutionException.

Klíčovým konceptem je to odeslání úkolu je odděleno od zásady provádění úkolu, který je popsán v Vykonavatel implementace. The spustitelný task je tedy schopen provést prostřednictvím nového vlákna, sdruženého vlákna, volajícího vlákna atd.

Všimněte si, že Vykonavatel je velmi omezený. Například nemůžete vypnout exekutora nebo zjistit, zda asynchronní úkol skončil. Spuštěný úkol také nelze zrušit. Z těchto a dalších důvodů poskytuje rámec Executor rozhraní ExecutorService, které se rozšiřuje Vykonavatel.

Pět z ExecutorServiceZvláště pozoruhodné jsou metody:

  • boolean awaitTermination (dlouhý časový limit, jednotka TimeUnit) blokuje volající vlákno, dokud všechny úkoly nedokončí spuštění po požadavku na vypnutí, nevyprší časový limit nebo není přerušeno aktuální vlákno, podle toho, co nastane dříve. Maximální čas čekání je určen Časový limita tato hodnota je vyjádřena v jednotka jednotky určené jednotkou TimeUnit výčet; například, TimeUnit.SECONDS. Tato metoda hodí java.lang.InterruptedException když je aktuální vlákno přerušeno. Vrací se skutečný když je exekutor ukončen a Nepravdivé když vyprší časový limit před ukončením.
  • boolean isShutdown () se vrací skutečný když byl exekutor zavřen.
  • vypnutí prázdnoty () iniciuje řádné vypnutí, při kterém jsou prováděny dříve zadané úkoly, ale nejsou přijímány žádné nové úkoly.
  • Budoucí odeslání (volatelný úkol) odešle úlohu s návratem hodnoty k provedení a vrátí a Budoucnost představující nevyřízené výsledky úkolu.
  • Budoucí odeslání (Spustitelný úkol) předkládá a Spustitelný úkol pro provedení a vrácení a Budoucnost představující tento úkol.

The Budoucnost interface představuje výsledek asynchronního výpočtu. Výsledek je znám jako a budoucnost protože to obvykle nebude k dispozici až do nějaké chvíle v budoucnosti. Můžete vyvolat metody pro zrušení úkolu, vrácení výsledku úkolu (neomezené čekání nebo vypršení časového limitu, když úkol nedokončil) a určení, zda byl úkol zrušen nebo byl dokončen.

The Vyvolatelné rozhraní je podobné Spustitelný rozhraní v tom, že poskytuje jedinou metodu popisující vykonanou úlohu. Na rozdíl od Spustitelnýje neplatný běh () metoda, Vyvolatelnéje V call () vyvolá výjimku metoda může vrátit hodnotu a vyvolat výjimku.

Exekutor tovární metody

V určitém okamžiku budete chtít získat exekutora. Rámec Exekutora dodává Exekutoři užitná třída pro tento účel. Exekutoři nabízí několik továrních metod pro získání různých druhů exekutorů, které nabízejí konkrétní zásady provádění podprocesů. Tady jsou tři příklady:

  • ExecutorService newCachedThreadPool () vytvoří fond vláken, který podle potřeby vytvoří nová vlákna, ale který znovu použije dříve vytvořená vlákna, když jsou k dispozici. Vlákna, která nebyla použita po dobu 60 sekund, jsou ukončena a odstraněna z mezipaměti. Tento fond podprocesů obvykle zlepšuje výkon programů, které provádějí mnoho krátkodobých asynchronních úkolů.
  • ExecutorService newSingleThreadExecutor () vytvoří exekutora, který používá jedno pracovní vlákno pracující mimo neomezenou frontu - úkoly se přidají do fronty a budou se spouštět postupně (současně není aktivní více než jeden úkol). Pokud toto vlákno končí selháním během provádění před vypnutím exekutora, bude vytvořeno nové vlákno, které zaujme jeho místo, když je třeba provést další úkoly.
  • ExecutorService newFixedThreadPool (int nThreads) vytvoří fond podprocesů, který znovu použije pevný počet podprocesů pracujících mimo sdílenou neohraničenou frontu. Nejvíce nVlákna vlákna aktivně zpracovávají úkoly. Pokud jsou odeslány další úkoly, když jsou aktivní všechna vlákna, čekají ve frontě, dokud není vlákno k dispozici. Pokud jakékoli vlákno končí selháním během provádění před vypnutím, bude vytvořeno nové vlákno, které zaujme jeho místo, když je třeba provést další úkoly. Vlákna fondu existují, dokud není zavřen exekutor.

Rámec Exekutora nabízí další typy (například ScheduledExecutorService rozhraní), ale typy, se kterými budete pravděpodobně pracovat nejčastěji, jsou ExecutorService, Budoucnost, Vyvolatelné, a Exekutoři.

Viz java.util.concurrent Javadoc prozkoumat další typy.

Práce s rámcem exekutora

Zjistíte, že s rámcem Exekutora je celkem snadné pracovat. V seznamu 2 jsem použil Vykonavatel a Exekutoři nahradit příklad serveru ze seznamu 1 více škálovatelnou alternativou založenou na fondu vláken.

Výpis 2. Server.java (verze 2)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; třída Server {static Executor pool = Executors.newFixedThreadPool (5); public static void main (String [] args) hodí IOException {ServerSocket socket = nový ServerSocket (9000); while (true) {final Socket s = socket.accept (); Runnable r = new Runnable () {@Override public void run () {doWork (s); }}; pool.execute (r); }} static void doWork (Socket s) {}}

Výpis 2 použití newFixedThreadPool (int) k získání exekutora založeného na fondu vláken, který znovu použije pět vláken. Rovněž nahrazuje new Thread (r) .start (); s pool.execute (r); pro provádění spustitelných úloh prostřednictvím kteréhokoli z těchto vláken.

Výpis 3 představuje další příklad, ve kterém aplikace čte obsah libovolné webové stránky. Vydá výsledné řádky nebo chybovou zprávu, pokud obsah není k dispozici do maximálně pěti sekund.