Einführung

In Teil I haben wir untersucht, wie Threads erstellt werden. Erinnern wir uns noch einmal. Besser zusammen: Java und die Thread-Klasse.  Teil IV – Callable, Future und Freunde – 1Ein 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 Runnabledie 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-catchgeprü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 Futuresind 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 getMethode, 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 monitoror wait, sondern als die bekannte park()Methode (da der LockSupportMechanismus 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 Supplierliefert. Es hat keine Parameter, aber es gibt etwas zurück. So versorgt es Dinge. A Consumerverbraucht. 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 CompletableFutureerschien in Java 1.8. Es implementiert die FutureSchnittstelle, dh unsere Aufgaben werden in Zukunft erledigt und wir können anrufen, get()um das Ergebnis zu erhalten. Es implementiert aber auch die CompletionStageSchnittstelle. 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: Besser zusammen: Java und die Thread-Klasse.  Teil IV – Callable, Future und Freunde – 2Hier 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 CompletableFutureauch 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 CompletableFuturebeginnt 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 CompletableFutureverwendet werden . Dementsprechend können Sie immer wissen, dass Sie Folgendes tun können : RunnableConsumersFunctionsCompletableFuture

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 CompletableFuturees bietet. Wir können das Ergebnis von a CompletableFuturemit 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 completedFutureaufgerufen 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+ BiConsumerals 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: Besser zusammen: Java und die Thread-Klasse.  Teil IV – Callable, Future und Freunde – 3Diese Methoden akzeptieren eine Alternative CompletableStageund werden auf der CompletableStagezuerst 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!