JavaRush /Blogue Java /Random-PT /Você não pode arruinar o Java com um thread: Parte IV - C...
Viacheslav
Nível 3

Você não pode arruinar o Java com um thread: Parte IV - Callable, Future e amigos

Publicado no grupo Random-PT

Introdução

Já vimos como os threads são criados na primeira parte . Vamos lembrar novamente. Você não pode estragar Java com um tópico: Parte IV - Callable, Future e amigos - 1Um thread é Threadalgo que roda nele run, então vamos usar o compilador online tutorialspoint java e executar o seguinte código:
public class HelloWorld {

    public static void main(String []args){
        Runnable task = () -> {
            System.out.println("Hello World");
        };
        new Thread(task).start();
    }
}
Esta é a única opção para executar uma tarefa em um thread?

java.util.concurrent.Callable

Acontece que java.lang.Runnable tem um irmão e seu nome é java.util.concurrent.Callable e ele nasceu em Java 1.5. Quais são as diferenças? Se olharmos mais de perto o JavaDoc desta interface, veremos que, ao contrário Runnable, a nova interface declara um método callque retorna um resultado. Além disso, por padrão, ele lança uma exceção. Ou seja, nos poupa da necessidade de escrever try-catchblocos para exceções verificadas. Já nada mal, certo? Agora temos Runnableuma nova tarefa:
Callable task = () -> {
	return "Hello, World!";
};
Mas o que fazer com isso? Por que precisamos de uma tarefa em execução em um thread que retorne um resultado? Obviamente, no futuro esperamos receber o resultado das ações que serão realizadas no futuro. Futuro em Inglês - Futuro. E existe uma interface exatamente com o mesmo nome:java.util.concurrent.Future

java.util.concurrent.Future

A interface java.util.concurrent.Future descreve uma API para trabalhar com tarefas cujos resultados pretendemos obter no futuro: métodos de obtenção de resultados, métodos de verificação de status. Estamos Futureinteressados ​​em sua implementação java.util.concurrent.FutureTask . Ou seja Task, é isso que será executado em Future. O que também é interessante nessa implementação é que ela implementa e Runnable. Você pode considerar isso uma espécie de adaptador do antigo modelo de trabalho com tarefas em threads e do novo modelo (novo no sentido de que apareceu no java 1.5). Aqui está um exemplo:
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());
    }
}
Como pode ser visto no exemplo, usando o método obtemos geto resultado do problema task. (!)Importante, que no momento em que o resultado é obtido pelo método, geta execução passa a ser síncrona. Que mecanismo você acha que será usado aqui? Isso mesmo, não há bloco de sincronização - portanto veremos WAITING no JVisualVM não como monitorou wait, mas como o mesmo park(já que o mecanismo é usado LockSupport).

Interfaces Funcionais

A seguir falaremos sobre as classes do Java 1.8, por isso seria útil fazer uma breve introdução. Vejamos o seguinte código:
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);
	}
};
Há muito código desnecessário, não é? Cada uma das classes declaradas executa uma única função, mas para descrevê-la usamos um monte de código auxiliar desnecessário. E os desenvolvedores Java também pensaram assim. Portanto, eles introduziram um conjunto de “interfaces funcionais” ( @FunctionalInterface) e decidiram que agora o próprio Java irá “pensar” em tudo para nós, exceto os importantes:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Supplier- fornecedor. Não possui parâmetros, mas retorna algo, ou seja, fornece. Consumer- consumidor. Ele pega algo como entrada (parâmetros) e faz algo com isso, ou seja, consome algo. Existe outra função. Pega algo como entrada (parâmetro s), faz algo e retorna algo. Como podemos ver, os genéricos são usados ​​ativamente. Se não tiver certeza, você pode lembrá-los e ler “ A teoria dos genéricos em Java ou como colocar parênteses na prática ”.

CompletávelFuturo

Com o passar do tempo, o Java 1.8 introduziu uma nova classe chamada CompletableFuture. Ele implementa a interface Future, ou seja, a nossa taskserá executada no futuro e poderemos executar gete obter o resultado. Mas ele também implementa alguns CompletionStage. Pela tradução seu propósito já fica claro: é uma certa etapa de algum tipo de cálculo. Uma breve introdução ao tópico pode ser encontrada na visão geral " Introdução ao CompletionStage e CompletableFuture ". Vamos direto ao ponto. Vejamos a lista de métodos estáticos disponíveis para nos ajudar a começar: Você não pode estragar Java com um tópico: Parte IV - Callable, Future e amigos - 2Aqui estão as opções para usá-los:
import java.util.concurrent.CompletableFuture;
public class App {
    public static void main(String []args) throws Exception {
        // CompletableFuture уже содержащий результат
        CompletableFuture<String> completed;
        completed = CompletableFuture.completedFuture("Просто meaning");
        // 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 "Значение";
        });
    }
}
Se executarmos este código, veremos que a criação CompletableFutureimplica o lançamento de toda a cadeia. Portanto, embora haja alguma semelhança com o SteamAPI do Java8, esta é a diferença entre essas abordagens. Por exemplo:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
	System.out.println("Executed");
	return value.toUpperCase();
});
Este é um exemplo da API Java 8 Stream (você pode ler mais sobre ela aqui " Guia da API Java 8 Stream em imagens e exemplos "). Se você executar este código, ele Executednão será exibido. Ou seja, ao criar um fluxo em Java, o fluxo não inicia imediatamente, mas espera até que seja necessário um valor dele. Mas CompletableFutureinicia a cadeia para execução imediatamente, sem esperar que seja solicitado o valor calculado. Acho que é importante entender isso. Portanto, temos CompletableFuture. Como podemos criar uma cadeia e que meios temos? Vamos lembrar das interfaces funcionais sobre as quais escrevemos anteriormente.
  • Temos uma função ( Function) que pega A e retorna B. Ela possui um único método - apply(apply).
  • Temos um consumidor ( Consumer) que aceita A e não retorna nada ( Void ). Possui apenas um método - accept(aceitar).
  • Temos código rodando em uma thread Runnableque não aceita nem retorna. Possui um único método - run(executar).
A segunda coisa a lembrar é que CompletalbeFutureem seu trabalho utiliza Runnableconsumidores e funções. Diante disso, você sempre pode lembrar que CompletableFuturepode fazer isso:
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);
}
Os métodos thenRuntêm thenApplyversões . thenAccept_ AsyncIsso significa que essas etapas serão executadas em uma nova thread. Será retirado de uma piscina especial, por isso não se sabe de antemão que tipo de fluxo será, novo ou antigo. Tudo depende de quão difíceis são as tarefas. Além desses métodos, existem mais três possibilidades interessantes. Para maior clareza, vamos imaginar que temos um determinado serviço que recebe uma mensagem de algum lugar e isso leva tempo:
public static class NewsService {
	public static String getMessage() {
		try {
			Thread.currentThread().sleep(3000);
			return "Message";
		} catch (InterruptedException e) {
			throw new IllegalStateException(e);
		}
	}
}
Agora, vamos dar uma olhada nos outros recursos que o CompletableFuture. Podemos combinar o resultado CompletableFuturecom o resultado de outro 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();
É importante notar que por padrão os threads serão threads daemon, portanto, para maior clareza get, costumamos esperar pelo resultado. E não podemos apenas combinar (combinar), mas também retornar CompletableFuture:
CompletableFuture.completedFuture(2L)
				.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
                               .thenAccept(result -> System.out.println(result));
Gostaria de observar aqui que, por questões de brevidade, o método foi usado CompletableFuture.completedFuture. Este método não cria uma nova thread, portanto o restante da cadeia será executado na mesma thread em que foi chamada completedFuture. Também existe um método thenAcceptBoth. É muito parecido com accept, mas se thenAcceptaceitar consumer, então thenAcceptBothaceita outro CompletableStage+ como entrada BiConsumer, ou seja consumer, que aceita 2 fontes como entrada, e não uma. Existe outra possibilidade interessante com a palavra Either: Você não pode estragar o Java com um thread: Parte IV - Callable, Future e amigos - 3Esses métodos aceitam uma alternativa CompletableStagee serão executados naquele CompletableStageque for executado primeiro. E gostaria de terminar esta análise com outro recurso interessante CompletableFuture- tratamento de erros.
CompletableFuture.completedFuture(2L)
				 .thenApply((a) -> {
					throw new IllegalStateException("error");
				 }).thenApply((a) -> 3L)
				 //.exceptionally(ex -> 0L)
				 .thenAccept(val -> System.out.println(val));
Este código não fará nada, porque... uma exceção será lançada e nada acontecerá. Mas se descomentarmos exceptionally, definimos o comportamento. CompletableFutureTambém recomendo assistir ao seguinte vídeo sobre este assunto : Na minha humilde opinião, esses vídeos são alguns dos mais visuais da Internet. Deveria ficar claro para eles como tudo funciona, que arsenal temos e por que tudo isso é necessário.

Conclusão

Esperamos que agora esteja claro como os threads podem ser usados ​​para recuperar cálculos depois de terem sido calculados. Material adicional: #Viacheslav
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION