Introdução
Já vimos como os threads são criados na
primeira parte . Vamos lembrar novamente.
Um thread é
Thread
algo 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
call
que retorna um resultado. Além disso, por padrão, ele lança uma exceção. Ou seja, nos poupa da necessidade de escrever
try-catch
blocos para exceções verificadas. Já nada mal, certo? Agora temos
Runnable
uma 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
Future
interessados 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
get
o resultado do problema
task
.
(!)Importante, que no momento em que o resultado é obtido pelo método,
get
a 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
monitor
ou
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
task
será executada no futuro e poderemos executar
get
e 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:
Aqui 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<String> completed;
completed = CompletableFuture.completedFuture("Просто meaning");
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 "Значение";
});
}
}
Se executarmos este código, veremos que a criação
CompletableFuture
implica 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
Executed
nã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
CompletableFuture
inicia 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
Runnable
que não aceita nem retorna. Possui um único método - run
(executar).
A segunda coisa a lembrar é que
CompletalbeFuture
em seu trabalho utiliza
Runnable
consumidores e funções. Diante disso, você sempre pode lembrar que
CompletableFuture
pode 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.runAsync(task)
.thenApply((v) -> longValue.get())
.thenApply(dateConverter)
.thenAccept(printer);
}
Os métodos
thenRun
têm
thenApply
versões .
thenAccept
_
Async
Isso 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
CompletableFuture
com 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
thenAccept
aceitar
consumer
, então
thenAcceptBoth
aceita 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
:
Esses métodos aceitam uma alternativa
CompletableStage
e serão executados naquele
CompletableStage
que 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)
.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.
CompletableFuture
També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
GO TO FULL VERSION