Einführung
In
Teil I haben wir untersucht, wie Threads erstellt werden. Erinnern wir uns noch einmal.
Ein Thread wird durch die Thread-Klasse dargestellt, deren
run()
Methode aufgerufen wird. Lassen Sie uns also den
Online-Java-Compiler Tutorialspoint verwenden und den folgenden Code ausführen:
public class HelloWorld {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello World");
};
new Thread(task).start();
}
}
Ist dies die einzige Möglichkeit, eine Aufgabe in einem Thread zu starten?
java.util.concurrent.Callable
Es stellt sich heraus, dass
java.lang.Runnable einen Bruder namens
java.util.concurrent.Callable hat , der in Java 1.5 auf die Welt kam. Was sind die Unterschiede? Wenn Sie sich das Javadoc für diese Schnittstelle genau ansehen, sehen wir, dass
Runnable
die neue Schnittstelle im Gegensatz zu eine
call()
Methode deklariert, die ein Ergebnis zurückgibt. Außerdem wird standardmäßig eine Ausnahme ausgelöst. Das heißt, es erspart uns die Blockierung
try-catch
geprüfter Ausnahmen. Nicht schlecht, oder? Jetzt haben wir eine neue Aufgabe statt
Runnable
:
Callable task = () -> {
return "Hello, World!";
};
Aber was machen wir damit? Warum brauchen wir eine Aufgabe, die in einem Thread ausgeführt wird und ein Ergebnis zurückgibt? Natürlich erwarten wir für alle in der Zukunft durchgeführten Aktionen, dass wir das Ergebnis dieser Aktionen in der Zukunft erhalten. Und wir haben eine Schnittstelle mit einem entsprechenden Namen:
java.util.concurrent.Future
java.util.concurrent.Future
Die
java.util.concurrent.Future- Schnittstelle definiert eine API für die Arbeit mit Aufgaben, deren Ergebnisse wir in Zukunft erhalten möchten: Methoden zum Abrufen eines Ergebnisses und Methoden zum Überprüfen des Status. Bezüglich
Future
sind wir an der Implementierung in der Klasse
java.util.concurrent.FutureTask interessiert . Dies ist die „Aufgabe“, die in ausgeführt wird
Future
. Was diese Implementierung noch interessanter macht, ist, dass sie auch Runnable implementiert. Sie können dies als eine Art Adapter zwischen dem alten Modell der Arbeit mit Aufgaben in Threads und dem neuen Modell betrachten (neu in dem Sinne, dass es in Java 1.5 erschien). Hier ist ein Beispiel:
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());
}
}
Wie Sie dem Beispiel entnehmen können, verwenden wir die
get
Methode, um das Ergebnis der Aufgabe zu erhalten.
Notiz:Wenn Sie das Ergebnis mithilfe der
get()
Methode erhalten, wird die Ausführung synchron! Welcher Mechanismus wird Ihrer Meinung nach hier zum Einsatz kommen? Es stimmt, es gibt keinen Synchronisationsblock.
Aus diesem Grund sehen wir WAITING in JVisualVM nicht als
monitor
or
wait
, sondern als die bekannte
park()
Methode (da der
LockSupport
Mechanismus verwendet wird).
Funktionale Schnittstellen
Als nächstes sprechen wir über Klassen aus Java 1.8, daher wäre es gut, eine kurze Einführung zu geben. Schauen Sie sich den folgenden Code an:
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);
}
};
Sehr viel zusätzlicher Code, finden Sie nicht? Jede der deklarierten Klassen führt eine Funktion aus, aber wir verwenden eine Menge zusätzlichen unterstützenden Code, um sie zu definieren. Und so dachten Java-Entwickler. Dementsprechend führten sie eine Reihe von „funktionalen Schnittstellen“ ein (
@FunctionalInterface
) und beschlossen, dass Java nun selbst das „Denken“ übernehmen würde und uns nur noch die wichtigen Dinge überlassen würden, um die wir uns kümmern müssten:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
A
Supplier
liefert. Es hat keine Parameter, aber es gibt etwas zurück. So versorgt es Dinge. A
Consumer
verbraucht. Es nimmt etwas als Eingabe (ein Argument) und macht etwas damit. Das Argument ist, was es verbraucht. Dann haben wir auch
Function
. Es nimmt Eingaben (Argumente) entgegen, führt etwas aus und gibt etwas zurück. Sie sehen, dass wir aktiv Generika einsetzen. Wenn Sie sich nicht sicher sind, können Sie sich mit „
Generika in Java: Wie man spitze Klammern in der Praxis verwendet “ eine Auffrischung verschaffen.
Abschließbare Zukunft
Die Zeit verging und eine neue Klasse namens
CompletableFuture
erschien in Java 1.8. Es implementiert die
Future
Schnittstelle, dh unsere Aufgaben werden in Zukunft erledigt und wir können anrufen,
get()
um das Ergebnis zu erhalten. Es implementiert aber auch die
CompletionStage
Schnittstelle. Der Name ist Programm: Dies ist eine bestimmte Phase einer Reihe von Berechnungen. Eine kurze Einführung in das Thema finden Sie in der Rezension hier: Einführung in CompletionStage und CompletableFuture. Kommen wir gleich zur Sache. Schauen wir uns die Liste der verfügbaren statischen Methoden an, die uns den Einstieg erleichtern werden:
Hier sind Optionen für deren Verwendung:
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";
});
}
}
Wenn wir diesen Code ausführen, werden wir sehen, dass das Erstellen von
CompletableFuture
auch das Starten einer ganzen Pipeline erfordert. Daher finden wir hier, mit einer gewissen Ähnlichkeit zur SteamAPI von Java8, den Unterschied zwischen diesen Ansätzen. Zum Beispiel:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
Dies ist ein Beispiel für die Stream-API von Java 8. Wenn Sie diesen Code ausführen, werden Sie feststellen, dass „Ausgeführt“ nicht angezeigt wird. Mit anderen Worten: Wenn ein Stream in Java erstellt wird, startet der Stream nicht sofort. Stattdessen wartet es darauf, dass jemand einen Nutzen daraus ziehen möchte. Aber
CompletableFuture
beginnt sofort mit der Ausführung der Pipeline, ohne darauf zu warten, dass jemand nach einem Wert fragt. Ich denke, das ist wichtig zu verstehen. Also, wir haben eine
CompletableFuture
. Wie können wir eine Pipeline (oder Kette) erstellen und über welche Mechanismen verfügen wir? Erinnern Sie sich an die funktionalen Schnittstellen, über die wir zuvor geschrieben haben.
- Wir haben ein
Function
, das ein A annimmt und ein B zurückgibt. Es hat eine einzige Methode: apply()
.
- Wir haben ein
Consumer
, das ein A annimmt und nichts zurückgibt (Void). Es gibt eine einzige Methode: accept()
.
- Wir haben
Runnable
, das auf dem Thread läuft, nichts entgegennimmt und nichts zurückgibt. Es gibt eine einzige Methode: run()
.
Das nächste, woran Sie sich erinnern sollten, ist, dass in seiner Arbeit , und
CompletableFuture
verwendet werden . Dementsprechend können Sie immer wissen, dass Sie Folgendes tun können :
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);
}
Die Methoden
thenRun()
,
thenApply()
und
thenAccept()
haben „Async“-Versionen. Dies bedeutet, dass diese Phasen in einem anderen Thread abgeschlossen werden. Dieser Thread wird aus einem speziellen Pool entnommen, sodass wir nicht im Voraus wissen, ob es sich um einen neuen oder alten Thread handelt. Es hängt alles davon ab, wie rechenintensiv die Aufgaben sind. Neben diesen Methoden gibt es noch drei weitere interessante Möglichkeiten. Stellen wir uns zur Verdeutlichung vor, dass wir einen bestimmten Dienst haben, der von irgendwoher eine Nachricht empfängt – und das braucht Zeit:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Werfen wir nun einen Blick auf andere Fähigkeiten, die
CompletableFuture
es bietet. Wir können das Ergebnis von a
CompletableFuture
mit dem Ergebnis eines anderen kombinieren
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();
Beachten Sie, dass Threads standardmäßig Daemon-Threads sind. Aus Gründen der Übersichtlichkeit
get()
warten wir daher auf das Ergebnis. Wir können nicht nur kombinieren
CompletableFutures
, sondern auch Folgendes zurückgeben
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
An dieser Stelle möchte ich darauf hinweisen, dass die
CompletableFuture.completedFuture()
Methode der Kürze halber verwendet wurde. Diese Methode erstellt keinen neuen Thread, sodass der Rest der Pipeline im selben Thread ausgeführt wird, in dem sie
completedFuture
aufgerufen wurde. Es gibt auch eine
thenAcceptBoth()
Methode. Es ist sehr ähnlich zu
accept()
, aber wenn
thenAccept()
es a akzeptiert
Consumer
,
thenAcceptBoth()
akzeptiert es ein anderes
CompletableStage
+
BiConsumer
als Eingabe, also a
consumer
, das zwei Quellen anstelle einer akzeptiert. Es gibt noch eine weitere interessante Fähigkeit, die Methoden bieten, deren Name das Wort „Either“ enthält:
Diese Methoden akzeptieren eine Alternative
CompletableStage
und werden auf der
CompletableStage
zuerst ausgeführten Methode ausgeführt. Abschließend möchte ich diese Rezension mit einer weiteren interessanten Funktion abschließen
CompletableFuture
: der Fehlerbehandlung.
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
//.exceptionally(ex -> 0L)
.thenAccept(val -> System.out.println(val));
Dieser Code wird nichts bewirken, da es eine Ausnahme gibt und nichts anderes passiert. Aber indem wir die „ausnahmsweise“-Anweisung auskommentieren, definieren wir das erwartete Verhalten. Apropos
CompletableFuture
: Ich empfehle Ihnen auch, sich das folgende Video anzusehen:
Meiner bescheidenen Meinung nach gehören dies zu den erklärendsten Videos im Internet. Sie sollten deutlich machen, wie das alles funktioniert, welche Tools uns zur Verfügung stehen und warum das alles nötig ist.
Abschluss
Hoffentlich ist jetzt klar, wie Sie Threads verwenden können, um Berechnungen zu erhalten, nachdem sie abgeschlossen sind. Zusätzliches Material:
Besser zusammen: Java und die Thread-Klasse. Teil I – Ausführungsthreads Gemeinsam besser: Java und die Thread-Klasse. Teil II – Synchronisierung Gemeinsam besser: Java und die Thread-Klasse. Teil III – Gemeinsam besser interagieren: Java und die Thread-Klasse. Teil V – Executor, ThreadPool, Fork/Join Gemeinsam besser: Java und die Thread-Klasse. Teil VI – Feuer los!
GO TO FULL VERSION