Útmutató a java.util.concurrent.Future webhelyhez

1. Áttekintés

Ebben a cikkben megismerjük Jövő. A Java 1.5 óta létező felület, amely nagyon hasznos lehet aszinkron hívásokkal és egyidejű feldolgozással.

2. Teremtés Határidős

Egyszerűen fogalmazva: Jövő osztály egy aszinkron számítás jövőbeli eredményét jelenti - egy olyan eredmény, amely végül megjelenik a Jövő miután a feldolgozás befejeződött.

Lássuk, hogyan lehet olyan módszereket írni, amelyek létrehozzák és visszaadják a Jövő példa.

A hosszú futású módszerek jó jelöltek az aszinkron feldolgozásra és a Jövő felület. Ez lehetővé teszi számunkra, hogy valamilyen más folyamatot hajtsunk végre, amíg a beágyazott feladatot várjuk Jövő teljesíteni.

Néhány példa olyan műveletekre, amelyek kihasználnák az aszinkron jellegét Jövő vannak:

  • számításigényes folyamatok (matematikai és tudományos számítások)
  • nagy adatstruktúrák (big data) manipulálása
  • távoli módszeres hívások (fájlok letöltése, HTML-selejtezés, webszolgáltatások).

2.1. Végrehajtás Határidős Val vel FutureTask

Példánkhoz egy nagyon egyszerű osztályt fogunk létrehozni, amely kiszámítja egy négyzet négyzetét Egész szám. Ez határozottan nem illik a „régóta futó” módszerek kategóriájába, de a Thread.sleep () hívd fel, hogy 1 másodpercig tartson:

public class SquareCalculator {private ExecutorService végrehajtó = Executors.newSingleThreadExecutor (); public Future számít (egész szám bevitele) {return végrehajtó.beküldés (() -> {Thread.sleep (1000); return input * input;}); }}

A számítást ténylegesen végrehajtó kód bitje a hívás() módszer, amelyet lambda kifejezésként adunk be. Amint láthatja, semmi különös nincs benne, kivéve a alvás() korábban említett hívás.

Érdekesebbé válik, amikor figyelmünket a Hívható és ExecutorService.

Hívható egy olyan felület, amely egy olyan feladatot képvisel, amely eredményt ad vissza, és amelynek egyetlen van hívás() módszer. Itt létrehoztunk egy példányt egy lambda kifejezéssel.

A .példány létrehozása Hívható nem visz minket sehová, mégis át kell adnunk ezt a példányt egy végrehajtónak, aki gondoskodni fog a feladat új szálon való elindításáról és visszaadja az értékeset Jövő tárgy. Ahol ExecutorService bejön.

Néhány módon megismerhetjük az an ExecutorService Például a legtöbbet a közüzemi osztály biztosítja Végrehajtók statikus gyári módszerek. Ebben a példában az alapszintet használtuk newSingleThreadExecutor (), ami egy ExecutorService képes egyszerre egyetlen szál kezelésére.

Ha van egy ExecutorService objektum, csak hívnunk kell Beküldés() elhaladva Hívható érvként. Beküldés() gondoskodik a feladat megkezdéséről és visszaadja a FutureTask objektum, amely a Jövő felület.

3. Fogyasztó Határidős

Eddig a pontig megtanultuk, hogyan kell létrehozni egy példányt Jövő.

Ebben a szakaszban megtudhatjuk, hogyan kell működni ezzel a példánnyal, az összes módszer feltárásával Jövő’S API.

3.1. Használata kész() és kap() az eredmények megszerzéséhez

Most hívnunk kell kiszámítja() és felhasználja a visszaküldöttet Jövő hogy megkapja az eredményt Egész szám. Két módszer a Jövő Az API segít nekünk ebben a feladatban.

Future.isDone () megmondja, hogy a végrehajtó befejezte-e a feladat feldolgozását. Ha a feladat befejeződött, akkor visszatér igaz különben visszatér hamis.

Az a módszer, amely a tényleges eredményt adja a számításból, az Future.get (). Figyelje meg, hogy ez a módszer a feladat befejezéséig blokkolja a végrehajtást, de a példánkban ez nem jelent problémát, mivel először a kész().

E két módszer használatával futtathatunk más kódokat, amíg várunk a fő feladat befejezésére:

Jövő jövő = új SquareCalculator (). Számolja ki (10); while (! future.isDone ()) {System.out.println ("Számítás ..."); Szál.alszik (300); } Egész eredmény = jövő.get ();

Ebben a példában egy egyszerű üzenetet írunk a kimenetre, hogy a felhasználó tudassa, hogy a program elvégzi a számítást.

A módszer, a metódus kap() a feladat befejezéséig blokkolja a végrehajtást. De emiatt nem kell aggódnunk, mivel példánk csak arra a pontra jut el, ahol kap() hívják, miután megbizonyosodott arról, hogy a feladat befejeződött. Tehát ebben a forgatókönyvben future.get () mindig azonnal visszatér.

Érdemes ezt megemlíteni kap() túlterhelt verziója van, amely időtúllépéshez és a TimeUnit érvként:

Egész eredmény = future.get (500, TimeUnit.MILLISECONDS);

A különbség get (hosszú, TimeUnit) és kap(), az, hogy az előbbi dob egy TimeoutException ha a feladat nem tér vissza a megadott időkorlát előtt.

3.2. Törlés a Jövő With megszünteti()

Tegyük fel, hogy kiváltottunk egy feladatot, de valamilyen oknál fogva már nem érdekel az eredmény. Tudjuk használni Future.cancel (logikai érték) mondani a végrehajtónak, hogy állítsa le a műveletet és szakítsa meg a mögöttes szálat:

Jövő jövő = új SquareCalculator (). Számolja ki (4); logikai törölt = jövő.törlés (igaz);

Példánk Jövő a fenti kódból soha nem fejezné be a működését. Sőt, ha megpróbálunk telefonálni kap() abból az esetből, a hívás után megszünteti(), az eredmény a CancellationException. Future.isCancelled () megmondja nekünk, ha a Jövő már törölték. Ez nagyon hasznos lehet a CancellationException.

Lehetséges, hogy felhívás megszünteti() nem sikerül. Ebben az esetben a visszaadott értéke az lesz hamis. Figyelje meg megszünteti() vesz egy logikai érték argumentumként - ez szabályozza, hogy a feladatot végrehajtó szál megszakadjon-e vagy sem.

4. Több többszálas cérna Medencék

A jelenlegi ExecutorService egyszálú, mivel az Executors.newSingleThreadExecutor programmal szerezték be. Ennek az „egyszálúságnak” a kiemelésére indítsunk két számítást egyszerre:

SquareCalculator squareCalculator = új SquareCalculator (); Future future1 = squareCalculator.calculate (10); Future future2 = squareCalculator.calculate (100); while (! (future1.isDone () && future2.isDone ())) {System.out.println (String.format ("jövő1% s és jövő2% s", jövő1.isDone ()? "kész": "nincs kész", future2.isDone ()? "kész": "nem kész")); Szál.alszik (300); } Egész eredmény1 = jövő1.get (); Egész eredmény2 = jövő2.get (); System.out.println (eredmény1 + "és" + eredmény2); squareCalculator.shutdown ();

Most elemezzük a kód kimenetét:

négyzet kiszámítása a következőre: 10 jövő1 nincs megcsinálva és a jövő2 nincs készítve a jövő1 nem történik meg és a jövő2 nem történik meg a jövő1 nem történik meg és a jövő2 nem történik meg a jövő1 nem történik meg a jövő2 nem történik meg a négyzet kiszámítása: a jövő2 nincs megcsinálva a jövő1 elkészül és a jövő2 nem készül el a jövő1 és a jövő2 nem készül el 100 és 10000

Világos, hogy a folyamat nem párhuzamos. Figyelje meg, hogy a második feladat csak az első feladat befejezése után kezdődik, így az egész folyamat körülbelül 2 másodpercet vesz igénybe.

Ahhoz, hogy programunk valóban több szálú legyen, a másik ízét kell használnunk ExecutorService. Nézzük meg, hogyan változik a példánk viselkedése, ha a gyári módszer által biztosított szálkészletet használjuk Executors.newFixedThreadPool ():

public class SquareCalculator {private ExecutorService végrehajtó = Executors.newFixedThreadPool (2); // ...}

A mi egyszerű változásunkkal SquareCalculator osztályban van egy végrehajtónk, amely képes 2 egyidejű szálat használni.

Ha pontosan ugyanazt az ügyfélkódot futtatjuk újra, akkor a következő kimenetet kapjuk:

négyzet kiszámítása a következőre: 10 négyzet kiszámítása a következőre: 100 jövő1 nincs megcsinálva és a jövő2 nincs megcsinálva a jövő1 és a jövő2 nem történik meg a jövő1 és a jövő2 nem történik meg a jövő1 és a jövő2 nem történik meg

Ez most sokkal jobban néz ki. Figyelje meg, hogyan kezdődik és fejeződik be a 2 feladat egyszerre, és az egész folyamat körülbelül 1 másodpercet vesz igénybe.

Vannak más gyári módszerek is, amelyek felhasználhatók szálkészletek létrehozására, mint pl Executors.newCachedThreadPool () hogy a korábban használt újrafelhasználások cérnas amikor rendelkezésre állnak, és Executors.newScheduledThreadPool () amely ütemezi a parancsok futtatását egy adott késés után.

További információ a ExecutorService, olvassa el a témának szentelt cikkünket.

5. A. Áttekintése ForkJoinTask

ForkJoinTask egy elvont osztály, amely megvalósítja Jövő és képes számos olyan feladat futtatására, amelyet kis számú tényleges szál üzemeltet ForkJoinPool.

Ebben a szakaszban gyorsan áttekintjük a ForkJoinPool. A témával kapcsolatos átfogó útmutatóért tekintse meg a Java Fork / Join Framework útmutatónkat.

Ezután az a fő jellemzője ForkJoinTask az, hogy a fő feladata elvégzéséhez szükséges munka részeként általában új részfeladatokat fog létrehozni. Új feladatokat generál hívással Villa() és minden eredményt összegyűjt azzal csatlakozik(), így az osztály neve.

Két elvont osztály létezik ForkJoinTask: RecursiveTask amely a befejezéskor értéket ad vissza, és RecursiveAction ami nem ad vissza semmit. Ahogy a nevek is utalnak rá, ezeket az osztályokat rekurzív feladatokhoz kell használni, például a fájlrendszer navigálásához vagy a komplex matematikai számításhoz.

Bővítsük az előző példánkat egy olyan osztály létrehozására, amely adott egy Egész szám, kiszámítja az összes faktoriális elemének négyzetét. Tehát például, ha átadjuk a 4-es számot a számológépünknek, akkor az eredményt a 4² + 3² + 2² + 1² összegéből kapjuk, ami 30.

Először is létre kell hoznunk a RecursiveTask és végrehajtja annak kiszámít() módszer. Itt írjuk meg üzleti logikánkat:

a public class FactorialSquareCalculator kiterjeszti a RecursiveTask {private Integer n; public FactorialSquareCalculator (n egész szám) {this.n = n; } @Orride védett egész számítás () {if (n <= 1) {return n; } FactorialSquareCalculator calculator = új FactorialSquareCalculator (n - 1); számológép.fork (); return n * n + számológép.join (); }}

Figyelje meg, hogyan érjük el a rekurzivitást az új példány létrehozásával FactorialSquareCalculator belül kiszámít(). Hívással Villa(), nem blokkoló módszer, kérdezzük ForkJoinPool hogy kezdeményezhesse ennek a részfeladatnak a végrehajtását.

A csatlakozik() A módszer a számítás eredményét adja vissza, amelyhez hozzáadjuk az éppen látogatott szám négyzetét.

Most csak létre kell hoznunk a ForkJoinPool a végrehajtás és a szálkezelés kezeléséhez:

ForkJoinPool forkJoinPool = új ForkJoinPool (); FactorialSquareCalculator calculator = új FactorialSquareCalculator (10); forkJoinPool.execute (számológép);

6. Következtetés

Ebben a cikkben átfogó képet kaptunk a Jövő felületen, meglátogatva annak minden módszerét Megtanultuk azt is, hogyan lehet kihasználni a szálkészletek erejét több párhuzamos művelet kiváltására. A főbb módszerek a ForkJoinTask osztály, Villa() és csatlakozik() röviden kitértek arra is.

Sok más remek cikkünk van a Java párhuzamos és aszinkron műveleteiről. Íme három közülük, amelyek szorosan kapcsolódnak a Jövő interfész (néhányukat már említjük a cikkben):

  • Útmutató CompletableFuture - a Jövő a Java 8-ban számos extra funkcióval
  • Útmutató a Java Fork / Join Framework-hez - további információ ForkJoinTask az 5. szakaszban ismertettük
  • Útmutató a Java-hoz ExecutorService - szentelt a ExecutorService felület

Ellenőrizze a cikkben használt forráskódot a GitHub-tárházban.