Wstęp
Przyjrzeliśmy się już sposobowi tworzenia wątków w
pierwszej części . Przypomnijmy jeszcze raz.
Wątek to
Thread
coś, co w nim działa
run
, więc skorzystajmy z
internetowego kompilatora java tutorialspoint i wykonajmy następujący kod:
public class HelloWorld {
public static void main(String []args){
Runnable task = () -> {
System.out.println("Hello World");
};
new Thread(task).start();
}
}
Czy to jedyna opcja uruchomienia zadania w wątku?
Java.util.concurrent.Wywoływalny
Okazuje się, że
java.lang.Runnable ma brata, który nazywa się
java.util.concurrent.Callable i urodził się w Javie 1.5. Jakie są różnice? Jeśli przyjrzymy się bliżej dokumentowi JavaDoc tego interfejsu, zobaczymy, że w przeciwieństwie do
Runnable
nowego interfejsu, nowy interfejs deklaruje metodę
call
zwracającą wynik. Ponadto domyślnie zgłasza wyjątek. Oznacza to, że oszczędza nam to konieczności pisania
try-catch
bloków dla sprawdzanych wyjątków. Nieźle już, prawda? Zamiast tego mamy teraz
Runnable
nowe zadanie:
Callable task = () -> {
return "Hello, World!";
};
Ale co z tym zrobić? Dlaczego w ogóle potrzebujemy zadania działającego w wątku, który zwraca wynik? Oczywiście w przyszłości spodziewamy się otrzymać wynik działań, które zostaną wykonane w przyszłości. Przyszłość w języku angielskim - Przyszłość. Istnieje interfejs o dokładnie tej samej nazwie:
java.util.concurrent.Future
java.util.concurrent.Future
Interfejs
java.util.concurrent.Future opisuje API do pracy z zadaniami, których wyniki planujemy uzyskać w przyszłości: metody uzyskiwania wyników, metody sprawdzania statusu. Jesteśmy
Future
zainteresowani jego wdrożeniem
java.util.concurrent.FutureTask . Oznacza to
Task
, że to właśnie zostanie wykonane w
Future
. Interesujące w tej implementacji jest również to, że implementuje i
Runnable
. Można to uznać za swego rodzaju adapter starego modelu pracy z zadaniami w wątkach i nowego modelu (nowego w tym sensie, że pojawił się w Javie 1.5). Oto przykład:
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());
}
}
Jak widać na przykładzie stosując metodę otrzymujemy
get
wynik z zadania
task
.
(!)Ważny, że w momencie uzyskania wyniku metodą
get
wykonanie staje się synchroniczne. Jak myślisz, jaki mechanizm zostanie tutaj zastosowany? Zgadza się, nie ma bloku synchronizacji - dlatego w JVisualVM zobaczymy
WAITING nie jako
monitor
lub
wait
, ale jako ten sam
park
(ponieważ zastosowany jest mechanizm
LockSupport
).
Interfejsy funkcjonalne
W dalszej części omówimy klasy z Javy 1.8, dlatego warto dokonać krótkiego wprowadzenia. Spójrzmy na następujący kod:
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);
}
};
Jest dużo niepotrzebnego kodu, prawda? Każda z zadeklarowanych klas pełni jedną funkcję, ale do jej opisania używamy całej masy niepotrzebnego kodu pomocniczego. Tak też myśleli programiści Java. Dlatego wprowadzili zestaw „interfejsów funkcjonalnych” (
@FunctionalInterface
) i zdecydowali, że teraz Java sama „wymyśli” za nas wszystko oprócz tych ważnych:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Supplier
- dostawca. Nie ma parametrów, ale coś zwraca, czyli dostarcza.
Consumer
- konsument. Bierze coś jako dane wejściowe (parametry s) i coś z tym robi, to znaczy coś zużywa. Jest jeszcze jedna funkcja. Pobiera coś jako dane wejściowe (parametr
s
), robi coś i coś zwraca. Jak widzimy, leki generyczne są aktywnie wykorzystywane. Jeśli nie jesteś pewien, możesz je zapamiętać i przeczytać „
Teoria rodzajów generycznych w Javie, czyli jak stosować nawiasy w praktyce ”.
Kompletna przyszłość
Z biegiem czasu Java 1.8 wprowadziła nową klasę o nazwie
CompletableFuture
. Implementuje interfejs
Future
, co oznacza, że nasz
task
zostanie wykonany w przyszłości i będziemy mogli wykonać
get
i uzyskać wynik. Ale on też wdraża niektóre pliki
CompletionStage
. Z tłumaczenia jego cel jest już jasny: jest to pewien etap pewnego rodzaju obliczeń. Krótkie wprowadzenie do tematu można znaleźć w przeglądzie „
Wprowadzenie do CompletionStage i CompletableFuture ”. Przejdźmy od razu do rzeczy. Przyjrzyjmy się liście dostępnych metod statycznych, które pomogą nam zacząć:
Oto opcje ich użycia:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String []args) throws Exception {
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Просто oznaczający");
CompletableFuture<Void> voidCompletableFuture;
voidCompletableFuture = CompletableFuture.runAsync(() -> {
System.out.println("run " + Thread.currentThread().getName());
});
CompletableFuture<String> supplier;
supplier = CompletableFuture.supplyAsync(() -> {
System.out.println("supply " + Thread.currentThread().getName());
return "Значение";
});
}
}
Jeśli wykonamy ten kod, zobaczymy, że utworzenie
CompletableFuture
oznacza uruchomienie całego łańcucha. Dlatego chociaż istnieje pewne podobieństwo do SteamAPI z Java8, jest to różnica między tymi podejściami. Na przykład:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
To jest przykład Java 8 Stream Api (więcej na ten temat możesz przeczytać tutaj „
Przewodnik po Java 8 Stream API w obrazach i przykładach ”). Jeśli uruchomisz ten kod,
Executed
nie zostanie on wyświetlony. Oznacza to, że podczas tworzenia strumienia w Javie strumień nie uruchamia się natychmiast, ale czeka, aż potrzebna będzie z niego wartość. Ale
CompletableFuture
natychmiast uruchamia łańcuch do wykonania, nie czekając, aż zostanie poproszony o obliczoną wartość. Myślę, że ważne jest, aby to zrozumieć. Mamy więc CompletableFuture. Jak możemy stworzyć łańcuch i jakie mamy środki? Pamiętajmy o interfejsach funkcjonalnych, o których pisaliśmy wcześniej.
- Mamy funkcję (
Function
), która przyjmuje A i zwraca B. Ma jedną metodę - apply
(zastosuj).
- Mamy konsumenta (
Consumer
), który akceptuje A i nic nie zwraca ( Void ). Ma tylko jedną metodę - accept
(akceptacja).
- Mamy kod działający w wątku
Runnable
, który nie akceptuje ani nie zwraca. Ma jedną metodę - run
(uruchom).
Drugą rzeczą do zapamiętania jest to, że
CompletalbeFuture
w swojej pracy wykorzystuje
Runnable
konsumentów i funkcje. Biorąc to pod uwagę, zawsze możesz pamiętać, że możesz
CompletableFuture
to zrobić:
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.runAsync(task)
.thenApply((v) -> longValue.get())
.thenApply(dateConverter)
.thenAccept(printer);
}
Metody
thenRun
mają
thenApply
wersje .
thenAccept
_
Async
Oznacza to, że etapy te zostaną wykonane w nowym wątku. Będzie pobierany ze specjalnego basenu, więc nie wiadomo z góry, jaki to będzie przepływ, nowy czy stary. Wszystko zależy od tego, jak trudne są zadania. Oprócz tych metod istnieją trzy bardziej interesujące możliwości. Dla jasności wyobraźmy sobie, że mamy pewną usługę, która skądś otrzymuje wiadomość i wymaga to czasu:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Przyjrzyjmy się teraz innym funkcjom, które
CompletableFuture
. Możemy połączyć wynik
CompletableFuture
z wynikiem innego
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();
Warto zaznaczyć, że domyślnie wątki będą wątkami demonicznymi, dlatego dla przejrzystości
get
czekamy na wynik. I możemy nie tylko połączyć (połączyć), ale także zwrócić
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
Tutaj chciałbym zauważyć, że dla zwięzłości zastosowano metodę
CompletableFuture.completedFuture
. Ta metoda nie tworzy nowego wątku, więc reszta łańcucha zostanie wykonana w tym samym wątku, w którym została wywołana
completedFuture
. Istnieje również metoda
thenAcceptBoth
. Jest bardzo podobny do
accept
, ale jeśli
thenAccept
akceptuje
consumer
, to
thenAcceptBoth
akceptuje inny
CompletableStage
+ jako wejście
BiConsumer
, to znaczy
consumer
, który akceptuje 2 źródła jako wejście, a nie jedno. Ze słowem wiąże się jeszcze jedna interesująca możliwość
Either
:
Metody te akceptują alternatywę
CompletableStage
i zostaną wykonane na tej
CompletableStage
, która zostanie wykonana jako pierwsza. Chciałbym zakończyć tę recenzję inną interesującą funkcją
CompletableFuture
- obsługą błędów.
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
.thenAccept(val -> System.out.println(val));
Ten kod nic nie zrobi, ponieważ... zostanie zgłoszony wyjątek i nic się nie stanie. Ale jeśli odkomentujemy
exceptionally
, zdefiniujemy zachowanie.
CompletableFuture
Polecam także obejrzenie poniższego filmu na ten temat :
Moim skromnym zdaniem te filmy są jednymi z najbardziej wizualnych w Internecie. Powinno być od nich jasne, jak to wszystko działa, jaki mamy arsenał i dlaczego to wszystko jest potrzebne.
Wniosek
Mamy nadzieję, że teraz jest jasne, w jaki sposób można używać wątków do pobierania obliczeń po ich obliczeniu. Dodatkowy materiał:
#Wiaczesław
GO TO FULL VERSION