Ú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.