Bevezetés
Az I. részben áttekintettük , hogyan jönnek létre a szálak. Emlékezzünk vissza még egyszer.
![Jobb együtt: Java és a Thread osztály. IV. rész – Hívható, jövő és barátok – 1]()
A szálat a Thread osztály képviseli, amelynek
run()
metódusa meghívásra kerül. Tehát használjuk a
Tutorialspoint online Java fordítót , és futtassuk a következő kódot:
public class HelloWorld {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello World");
};
new Thread(task).start();
}
}
Ez az egyetlen lehetőség egy feladat elindítására egy szálon?
java.util.concurrent.Callable
Kiderült, hogy
a java.lang.Runnable-nek van egy java.util.concurrent.Callable nevű testvére , aki a Java 1.5-ben jött a világra. Mik a különbségek? Ha alaposan megnézi ennek a felületnek a Javadoc-ot, azt látjuk, hogy az új felülettel ellentétben
Runnable
az új felület olyan metódust deklarál
call()
, amely eredményt ad vissza. Ezenkívül alapértelmezés szerint kivételt dob. Vagyis megkímél minket attól, hogy le kell
try-catch
tiltanunk az ellenőrzött kivételeket. Nem rossz, igaz? Most új feladatunk van ahelyett
Runnable
:
Callable task = () -> {
return "Hello, World!";
};
De mit kezdjünk vele? Miért van szükségünk egy olyan szálon futó feladatra, amely eredményt ad vissza? Nyilvánvaló, hogy a jövőben végrehajtott bármely tevékenység esetén elvárjuk, hogy a jövőben megkapjuk az adott cselekvés eredményét. És van egy interfészünk a megfelelő névvel:
java.util.concurrent.Future
java.util.concurrent.Future
A
java.util.concurrent.Future interfész API-t definiál az olyan feladatokkal való munkavégzéshez, amelyek eredményeit a jövőben tervezzük megkapni: az eredmény elérésének és az állapot ellenőrzésének módszerei. Ami a -t illeti
Future
, érdekel minket annak megvalósítása a
java.util.concurrent.FutureTask osztályban. Ez az a "Feladat", amely a következőben kerül végrehajtásra
Future
. Ezt az implementációt még érdekesebbé teszi, hogy a Runnable-t is megvalósítja. Ezt egyfajta adapternek tekintheti a szálakon végzett feladatokkal való munkavégzés régi modellje és az új modell között (új modell abban az értelemben, hogy a Java 1.5-ben jelent meg). Íme egy példa:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class HelloWorld {
public static void main(String[] args) throws Exception {
Callable task = () -> {
return "Hello, World!";
};
FutureTask<String> future = new FutureTask<>(task);
new Thread(future).start();
System.out.println(future.get());
}
}
Ahogy a példából is látható, a
get
módszert arra használjuk, hogy megkapjuk a feladat eredményét.
Jegyzet:amikor a metódussal megkapod az eredményt
get()
, a végrehajtás szinkron lesz! Ön szerint milyen mechanizmust fognak itt használni? Igaz, nincs szinkronizálási blokk.
Ez az oka annak, hogy a JVisualVM-ben a WAITING-t nem a
monitor
vagy -ként fogjuk látni
wait
, hanem a megszokott
park()
metódusként (mivel a
LockSupport
mechanizmust használják).
Funkcionális interfészek
Ezután a Java 1.8 osztályairól fogunk beszélni, ezért jól tesszük, ha röviden bemutatjuk. Nézd meg a következő kódot:
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "String";
}
};
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
Function<String, Integer> converter = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.valueOf(s);
}
};
Sok-sok extra kód, nem mondanád? A deklarált osztályok mindegyike egy funkciót hajt végre, de ennek meghatározásához egy csomó extra támogató kódot használunk. És így gondolták a Java fejlesztők. Ennek megfelelően bevezettek egy sor "funkcionális interfészt" (
@FunctionalInterface
), és úgy döntöttek, hogy most már maga a Java végzi a "gondolkodást", csak a fontos dolgokat hagyva nekünk aggódni:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Egy
Supplier
kellékek. Paraméterei nincsenek, de visszaad valamit. Így látja el a dolgokat. A
Consumer
fogyaszt. Valamit inputnak (érvnek) vesz, és csinál vele valamit. Az érv az, hogy mit fogyaszt. Akkor nekünk is van
Function
. Bemeneteket (érveket) vesz, csinál valamit, és visszaad valamit. Látható, hogy aktívan használunk generikus gyógyszereket. Ha nem biztos benne, felfrissülhet, ha elolvassa a "
Generics in Java: a szögletes zárójelek használata a gyakorlatban " című részt.
CompletableFuture
Telt-múlt az idő, és egy új osztály jelent
CompletableFuture
meg a Java 1.8-ban. Ez valósítja meg a
Future
felületet, azaz a feladataink a jövőben elkészülnek, és
get()
az eredményért telefonálhatunk. De az interfészt is megvalósítja
CompletionStage
. A név mindent elárul: ez egy bizonyos szakasza egy bizonyos számítási sorozatnak. A téma rövid bevezetője az itt található áttekintésben található: Introduction to CompletionStage és CompletableFuture. Térjünk a lényegre. Nézzük meg a rendelkezésre álló statikus módszerek listáját, amelyek segítenek az indulásban:
![Jobb együtt: Java és a Thread osztály. IV. rész – Hívható, jövő és barátok – 2]()
Íme a használatukra vonatkozó lehetőségek:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String[] args) throws Exception {
// A CompletableFuture that already contains a Result
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Just a value");
// A CompletableFuture that runs a new thread from Runnable. That's why it's Void
CompletableFuture<Void> voidCompletableFuture;
voidCompletableFuture = CompletableFuture.runAsync(() -> {
System.out.println("run " + Thread.currentThread().getName());
});
// A CompletableFuture that starts a new thread whose result we'll get from a Supplier
CompletableFuture<String> supplier;
supplier = CompletableFuture.supplyAsync(() -> {
System.out.println("supply " + Thread.currentThread().getName());
return "Value";
});
}
}
Ha végrehajtjuk ezt a kódot, látni fogjuk, hogy a létrehozása
CompletableFuture
egy teljes folyamat elindítását is magában foglalja. Ezért a Java8 SteamAPI-jához való bizonyos hasonlóság mellett itt találjuk meg a különbséget e megközelítések között. Például:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
Ez egy példa a Java 8 Stream API-jára. Ha futtatja ezt a kódot, látni fogja, hogy a „Végrehajtva” nem jelenik meg. Más szóval, amikor egy adatfolyamot Java nyelven hoz létre, az adatfolyam nem indul el azonnal. Ehelyett arra vár, hogy valaki értéket akarjon tőle. De
CompletableFuture
azonnal megkezdi a csővezeték végrehajtását, anélkül, hogy megvárná, hogy valaki értéket kérjen tőle. Szerintem ezt fontos megérteni. Szóval van egy
CompletableFuture
. Hogyan készítsünk csővezetéket (vagy láncot), és milyen mechanizmusokkal rendelkezünk? Emlékezzünk vissza azokra a funkcionális interfészekre, amelyekről korábban írtunk.
- Van a
Function
, amely egy A-t vesz fel, és egy B-t ad vissza. Egyetlen módszere van: apply()
.
- Van olyan
Consumer
, amelyik egy A-t vesz fel, és nem ad vissza semmit (Vid). Egyetlen módszere van: accept()
.
- Van
Runnable
, amely a szálon fut, és nem vesz el semmit, és nem ad vissza semmit. Egyetlen módszere van: run()
.
A következő dolog, amit meg kell jegyeznünk, az, hogy a , és -t
CompletableFuture
használja a munkájában. Ennek megfelelően mindig tudhatja, hogy a következőket teheti :
Runnable
Consumers
Functions
CompletableFuture
public static void main(String[] args) throws Exception {
AtomicLong longValue = new AtomicLong(0);
Runnable task = () -> longValue.set(new Date().getTime());
Function<Long, Date> dateConverter = (longvalue) -> new Date(longvalue);
Consumer<Date> printer = date -> {
System.out.println(date);
System.out.flush();
};
// CompletableFuture computation
CompletableFuture.runAsync(task)
.thenApply((v) -> longValue.get())
.thenApply(dateConverter)
.thenAccept(printer);
}
A
thenRun()
,
thenApply()
, és
thenAccept()
metódusoknak "Async" verziója van. Ez azt jelenti, hogy ezek a szakaszok egy másik szálon fognak befejeződni. Ez a szál egy speciális gyűjteményből lesz átvéve – így nem tudjuk előre, hogy új vagy régi szál lesz-e. Minden attól függ, hogy a feladatok mennyire számításigényesek. Ezeken a módszereken kívül még három érdekes lehetőség adódik. Az egyértelműség kedvéért képzeljük el, hogy van egy bizonyos szolgáltatásunk, amely valahonnan valamilyen üzenetet kap – és ez időbe telik:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Most pedig vessünk egy pillantást a további képességekre, amelyeket
CompletableFuture
biztosít. Összevonhatjuk a eredményét egy
CompletableFuture
másik eredményével
CompletableFuture
:
Supplier newsSupplier = () -> NewsService.getMessage();
CompletableFuture<String> reader = CompletableFuture.supplyAsync(newsSupplier);
CompletableFuture.completedFuture("!!")
.thenCombine(reader, (a, b) -> b + a)
.thenAccept(result -> System.out.println(result))
.get();
Vegye figyelembe, hogy a szálak alapértelmezés szerint démonszálak, ezért az egyértelműség kedvéért várunk
get()
az eredményre. Nem csak kombinálhatjuk
CompletableFutures
, hanem vissza is küldhetjük
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
Itt szeretném megjegyezni, hogy a
CompletableFuture.completedFuture()
módszert a tömörség kedvéért használták. Ez a metódus nem hoz létre új szálat, így a folyamat többi része ugyanazon a szálon lesz végrehajtva, ahol
completedFuture
meghívásra került. Van egy
thenAcceptBoth()
módszer is. Nagyon hasonló a -hoz
accept()
, de ha
thenAccept()
elfogad egy -t
Consumer
, akkor egy másik +-t
thenAcceptBoth()
fogad be bemenetként, azaz a-t , amely 2 forrást vesz fel egy helyett. Egy másik érdekes képességet kínálnak a metódusok, amelyek nevében benne van az "vagy" szó: ezek a metódusok elfogadnak egy alternatívát , és az elsőként végrehajtotton futnak le . Végül szeretném befejezni ezt az áttekintést egy másik érdekes tulajdonsággal : a hibakezeléssel.
CompletableStage
BiConsumer
consumer
![Jobb együtt: Java és a Thread osztály. IV. rész – Hívható, jövő és barátok – 3]()
CompletableStage
CompletableStage
CompletableFuture
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
//.exceptionally(ex -> 0L)
.thenAccept(val -> System.out.println(val));
Ez a kód nem fog semmit, mert lesz kivétel, és semmi más nem fog történni. De a "kivételesen" állítás megjegyzésének megszüntetésével meghatározzuk az elvárt viselkedést. Apropó
CompletableFuture
, javaslom a következő videó megtekintését is:
Szerény véleményem szerint ezek a legmagyarázatosabb videók közé tartoznak az interneten. Világossá kell tenniük, hogyan működik mindez, milyen eszköztár áll rendelkezésünkre, és miért van szükség erre.
Következtetés
Remélhetőleg most már világos, hogyan használhatja a szálakat a számítások elvégzésére azok befejezése után. Kiegészítő anyag:
Jobb együtt: Java és a Thread osztály. I. rész – A végrehajtás szálai Jobb együtt: Java és a Thread osztály. II. rész – Szinkronizálás Jobb együtt: Java és a Thread osztály. III. rész – Interakció Jobb együtt: Java és a szál osztály. V. rész – Végrehajtó, ThreadPool, Fork/Join Better together: Java és a Thread osztály. VI. rész – Tüzet el!
GO TO FULL VERSION