JavaRush /Blog Java /Random-PL /Nie możesz zrujnować Javy wątkiem: Część IV - Callable, F...
Viacheslav
Poziom 3

Nie możesz zrujnować Javy wątkiem: Część IV - Callable, Future i przyjaciele

Opublikowano w grupie Random-PL

Wstęp

Przyjrzeliśmy się już sposobowi tworzenia wątków w pierwszej części . Przypomnijmy jeszcze raz. Javy nie zepsujesz wątkiem: Część IV - Callable, Future and friends - 1Wątek to Threadcoś, 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 Runnablenowego interfejsu, nowy interfejs deklaruje metodę callzwracającą wynik. Ponadto domyślnie zgłasza wyjątek. Oznacza to, że oszczędza nam to konieczności pisania try-catchbloków dla sprawdzanych wyjątków. Nieźle już, prawda? Zamiast tego mamy teraz Runnablenowe 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 Futurezainteresowani 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 getwynik z zadania task. (!)Ważny, że w momencie uzyskania wyniku metodą getwykonanie staje się synchroniczne. Jak myślisz, jaki mechanizm zostanie tutaj zastosowany? Zgadza się, nie ma bloku synchronizacji - dlatego w JVisualVM zobaczymy WAITING nie jako monitorlub 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 taskzostanie wykonany w przyszłości i będziemy mogli wykonać geti 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ąć: Javy nie można zepsuć wątkiem: Część IV - Callable, Future and friends - 2Oto opcje ich użycia:
import java.util.concurrent.CompletableFuture;
public class App {
    public static void main(String []args) throws Exception {
        // CompletableFuture уже содержащий результат
        CompletableFuture<String> completed;
        completed = CompletableFuture.completedFuture("Просто oznaczający");
        // 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 "Значение";
        });
    }
}
Jeśli wykonamy ten kod, zobaczymy, że utworzenie CompletableFutureoznacza 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, Executednie 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 CompletableFuturenatychmiast 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 CompletalbeFuturew swojej pracy wykorzystuje Runnablekonsumentów i funkcje. Biorąc to pod uwagę, zawsze możesz pamiętać, że możesz CompletableFutureto 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 computation
        CompletableFuture.runAsync(task)
                         .thenApply((v) -> longValue.get())
                         .thenApply(dateConverter)
                         .thenAccept(printer);
}
Metody thenRunmają thenApplywersje . thenAccept_ AsyncOznacza 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 CompletableFuturez 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 getczekamy 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 thenAcceptakceptuje consumer, to thenAcceptBothakceptuje 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: Nie możesz zrujnować Javy wątkiem: Część IV - Callable, Future and friends - 3Metody te akceptują alternatywę CompletableStagei 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)
				 //.exceptionally(ex -> 0L)
				 .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. CompletableFuturePolecam 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
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION