JavaRush /Java-Blog /Random-DE /Sie können Java nicht mit einem Thread ruinieren: Teil IV...
Viacheslav
Level 3

Sie können Java nicht mit einem Thread ruinieren: Teil IV – Callable, Future und Freunde

Veröffentlicht in der Gruppe Random-DE

Einführung

Wie Threads erstellt werden, haben wir uns bereits im ersten Teil angeschaut . Erinnern wir uns noch einmal. Man kann Java nicht mit einem Thread verderben: Teil IV – Callable, Future und Freunde – 1Ein Thread ist Threadetwas, das darin ausgeführt wird run. Verwenden wir also den Java-Online-Compiler „tutorialspoint“ und führen Sie den folgenden Code aus:
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 auszuführen?

java.util.concurrent.Callable

Es stellt sich heraus, dass java.lang.Runnable einen Bruder hat und dieser java.util.concurrent.Callable heißt und in Java 1.5 geboren wurde. Was sind die Unterschiede? Wenn wir uns das JavaDoc dieser Schnittstelle genauer ansehen, sehen wir, dass Runnabledie neue Schnittstelle im Gegensatz zu eine Methode deklariert call, die ein Ergebnis zurückgibt. Außerdem wird standardmäßig eine Ausnahme ausgelöst. Das heißt, es erspart uns die Notwendigkeit, try-catchBlöcke für geprüfte Ausnahmen zu schreiben. Nicht schon schlecht, oder? Jetzt haben wir Runnablestattdessen eine neue Aufgabe:
Callable task = () -> {
	return "Hello, World!";
};
Aber was tun damit? Warum brauchen wir überhaupt eine Aufgabe, die in einem Thread ausgeführt wird und ein Ergebnis zurückgibt? Offensichtlich erwarten wir in Zukunft, dass wir das Ergebnis der Aktionen erhalten, die in der Zukunft durchgeführt werden. Zukunft auf Englisch - Zukunft. Und es gibt eine Schnittstelle mit genau demselben Namen:java.util.concurrent.Future

java.util.concurrent.Future

Die java.util.concurrent.Future- Schnittstelle beschreibt eine API für die Arbeit mit Aufgaben, deren Ergebnisse wir in Zukunft erhalten möchten: Methoden zum Erhalten von Ergebnissen, Methoden zum Überprüfen des Status. Wir Futuresind an der Implementierung java.util.concurrent.FutureTask interessiert . Das heißt Task, das ist es, was ausgeführt wird Future. Das Interessante an dieser Implementierung ist auch, dass sie und implementiert Runnable. Sie können dies als eine Art Adapter des alten Modells der Arbeit mit Aufgaben in Threads und des neuen Modells 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 aus dem Beispiel ersichtlich ist, erhalten wir mit der Methode getdas Ergebnis des Problems task. (!)Wichtig, dass in dem Moment, in dem das Ergebnis mit der Methode erhalten wird, getdie Ausführung synchron wird. Welcher Mechanismus wird Ihrer Meinung nach hier zum Einsatz kommen? Das ist richtig, es gibt keinen Synchronisationsblock – daher sehen wir WAITING in JVisualVM nicht als monitoror wait, sondern als genau dasselbe park(da der Mechanismus verwendet wird LockSupport).

Funktionale Schnittstellen

Als nächstes werden wir über Klassen aus Java 1.8 sprechen, daher wäre es sinnvoll, eine kurze Einführung zu geben. Schauen wir uns 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);
	}
};
Es gibt eine Menge unnötigen Code, nicht wahr? Jede der deklarierten Klassen führt eine einzelne Funktion aus, aber um sie zu beschreiben, verwenden wir eine Menge unnötigen Hilfscodes. Und das dachten sich auch die Java-Entwickler. Deshalb führten sie eine Reihe von „funktionalen Schnittstellen“ ein ( @FunctionalInterface) und beschlossen, dass Java jetzt selbst alles für uns „ausdenken“ wird, außer den wichtigen:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Supplier- Anbieter. Es hat keine Parameter, aber es gibt etwas zurück, das heißt, es liefert es. Consumer- Verbraucher. Es nimmt etwas als Eingabe (Parameter s) und macht etwas damit, das heißt, es verbraucht etwas. Es gibt noch eine weitere Funktion. Es nimmt etwas als Eingabe (Parameter s), führt etwas aus und gibt etwas zurück. Wie wir sehen, werden Generika aktiv eingesetzt. Wenn Sie sich nicht sicher sind, können Sie sich diese merken und „ Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt “ lesen.

Abschließbare Zukunft

Im Laufe der Zeit führte Java 1.8 eine neue Klasse namens ein CompletableFuture. Es implementiert die Schnittstelle Future, was bedeutet, dass unsere Schnittstelle taskin Zukunft ausgeführt wird und wir sie ausführen getund das Ergebnis erhalten können. Aber er setzt auch einiges um CompletionStage. Aus der Übersetzung geht bereits sein Zweck hervor: Es handelt sich um eine bestimmte Stufe einer Berechnung. Eine kurze Einführung in das Thema finden Sie in der Übersicht „ Einführung in CompletionStage und CompletableFuture “. Kommen wir gleich zur Sache. Schauen wir uns die Liste der verfügbaren statischen Methoden an, um uns den Einstieg zu erleichtern: Man kann Java nicht mit einem Thread verderben: Teil IV – Callable, Future und Freunde – 2Hier sind die Optionen für deren Verwendung:
import java.util.concurrent.CompletableFuture;
public class App {
    public static void main(String []args) throws Exception {
        // CompletableFuture уже содержащий результат
        CompletableFuture<String> completed;
        completed = CompletableFuture.completedFuture("Просто Bedeutung");
        // CompletableFuture, запускающий (run) новый поток с Runnable, поэтому он Void
        CompletableFuture<Void> voidCompletableFuture;
        voidCompletableFuture = CompletableFuture.runAsync(() -> {
            System.out.println("run " + Thread.currentThread().getName());
        });
        // CompletableFuture, запускающий новый поток, результат которого возьмём у Supplier
        CompletableFuture<String> supplier;
        supplier = CompletableFuture.supplyAsync(() -> {
            System.out.println("supply " + Thread.currentThread().getName());
            return "Значение";
        });
    }
}
Wenn wir diesen Code ausführen, werden wir sehen, dass CompletableFuturebei der Erstellung die gesamte Kette gestartet wird. Daher besteht zwar eine gewisse Ähnlichkeit mit der SteamAPI von Java8, dies ist jedoch der 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 Java 8 Stream API (mehr darüber können Sie hier lesen „ Java 8 Stream API Guide in Bildern und Beispielen “). Wenn Sie diesen Code ausführen, Executedwird er nicht angezeigt. Das heißt, wenn ein Stream in Java erstellt wird, startet der Stream nicht sofort, sondern wartet, bis ein Wert daraus benötigt wird. Aber CompletableFuturees startet die Kette sofort zur Ausführung, ohne darauf zu warten, dass sie nach dem berechneten Wert gefragt wird. Ich denke, es ist wichtig, das zu verstehen. Wir haben also CompletableFuture. Wie können wir eine Kette aufbauen und welche Mittel stehen uns zur Verfügung? Erinnern wir uns an die funktionalen Schnittstellen, über die wir zuvor geschrieben haben.
  • Wir haben eine Funktion ( Function), die A annimmt und B zurückgibt. Sie hat eine einzige Methode – apply(apply).
  • Wir haben einen Verbraucher ( Consumer), der A akzeptiert und nichts zurückgibt (Void). Es gibt nur eine Methode – accept(akzeptieren).
  • Wir haben Code, der in einem Thread ausgeführt wird Runnable, der weder akzeptiert noch zurückgibt. Es gibt eine einzige Methode – run(run).
Das Zweite, woran man denken sollte, ist, dass es bei seiner Arbeit Verbraucher und Funktionen CompletalbeFutureverwendet . RunnableVor diesem Hintergrund können Sie sich immer daran erinnern, dass Sie CompletableFutureFolgendes tun können:
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);
}
Methoden thenRunhaben thenApplyVersionen . thenAccept_ AsyncDies bedeutet, dass diese Phasen in einem neuen Thread ausgeführt werden. Es wird aus einem speziellen Becken entnommen, daher ist nicht im Voraus bekannt, um welche Art von Durchfluss es sich handelt, ob neu oder alt. Es hängt alles davon ab, wie schwierig 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);
		}
	}
}
Schauen wir uns nun die anderen Funktionen an, die CompletableFuture. Wir können das Ergebnis 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();
Es ist erwähnenswert, dass die Threads standardmäßig Daemon-Threads sind. Aus Gründen der Übersichtlichkeit getwarten wir daher auf das Ergebnis. Und wir können nicht nur kombinieren (kombinieren), sondern auch zurückgeben CompletableFuture:
CompletableFuture.completedFuture(2L)
				.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
                               .thenAccept(result -> System.out.println(result));
Hier möchte ich darauf hinweisen, dass der Kürze halber die Methode verwendet wurde CompletableFuture.completedFuture. Diese Methode erstellt keinen neuen Thread, daher wird der Rest der Kette in demselben Thread ausgeführt, in dem sie aufgerufen wurde completedFuture. Es gibt auch eine Methode thenAcceptBoth. Es ist sehr ähnlich zu accept, aber wenn thenAcceptes akzeptiert consumer, dann thenAcceptBothakzeptiert es ein anderes CompletableStage+ als Eingabe BiConsumer, das heißt consumer, das zwei Quellen als Eingabe akzeptiert, nicht eine. Es gibt noch eine weitere interessante Möglichkeit mit dem Wort Either: Man kann Java nicht mit einem Thread ruinieren: Teil IV – Callable, Future und Freunde – 3Diese Methoden akzeptieren eine Alternative CompletableStageund werden auf der CompletableStagezuerst ausgeführten ausgeführt. Und ich möchte 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, weil... Es wird eine Ausnahme ausgelöst und es passiert nichts. Wenn wir jedoch den Kommentar entfernen exceptionally, definieren wir das Verhalten. CompletableFutureIch empfehle außerdem, sich das folgende Video zu diesem Thema anzusehen: Meiner bescheidenen Meinung nach gehören diese Videos zu den anschaulichsten im Internet. Aus ihnen sollte klar hervorgehen, wie das alles funktioniert, über welches Arsenal wir verfügen und warum es alles benötigt wird.

Abschluss

Hoffentlich ist jetzt klar, wie Threads verwendet werden können, um Berechnungen abzurufen, nachdem sie berechnet wurden. Zusätzliches Material: #Wjatscheslaw
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION