Java Callable e Future
Retorne valores de tarefas com Callable e consuma-os de forma assíncrona com Future — aguarde, defina timeout, cancele e propague exceções.
Runnable permite que uma thread execute um trabalho. Mas não permite que o trabalho retorne um valor ou lance uma exceção verificada. O par que faz isso é Callable<V> (o produtor) e Future<V> (o consumidor). Você submete um Callable<V> a um ExecutorService e recebe de volta um Future<V>, que é seu identificador para: aguardar o resultado, ler o valor, capturar a exceção da tarefa ou cancelá-la.
Esta é a API mais básica com suporte a resultados no conjunto de ferramentas concorrentes do Java. O próximo capítulo, CompletableFuture, adiciona encadeamentos, combinadores e pipelines; mas o contrato — "um resultado assíncrono que você pode aguardar" — foi o que Future definiu primeiro, e ainda é a ferramenta certa para o simples "vá fazer isso e me avise quando terminar."
Callable<V> — Runnable com tipo de retorno
A interface:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}As duas diferenças em relação ao Runnable:
- Retorna
V(o parâmetro de tipo). - Pode lançar qualquer
Exception— incluindo exceções verificadas.
Assim como Runnable, é uma interface funcional — lambdas e referências de método funcionam:
Callable<Integer> compute = () -> {
Thread.sleep(100);
return 42;
};
Callable<String> read = () -> Files.readString(Path.of("config.txt")); // can throw IOException
Callable<List<Order>> query = () -> repo.findAll(); // can throw SQLExceptionCallable é a forma certa para qualquer trabalho do tipo "vá fazer isso e me traga um valor". Runnable é a forma certa apenas quando você genuinamente não se importa com um resultado.
Future<V> — o identificador para um resultado assíncrono
Quando você faz submit de um Callable<V>, o executor retorna um Future<V>:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}Cinco métodos. Três você usará com frequência.
get()
Bloqueia a thread chamadora até que a tarefa seja concluída e, em seguida, retorna o resultado:
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> { Thread.sleep(100); return 42; });
Integer value = f.get(); // blocks until done; returns 42get() lança três coisas que você precisa tratar:
InterruptedException— a thread chamadora foi interrompida enquanto aguardava. Tratamento padrão: redefina o sinalizador de interrupção e propague.ExecutionException— a própria tarefa lançou algo. A exceção original está encapsulada; acesse-a via.getCause().CancellationException— alguém chamoucancel()no future.
Uma forma comum:
try {
Integer v = f.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // the real exception the task threw
// ... handle cause ...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// ... bail out cooperatively ...
}get(timeout, unit)
Igual ao get(), mas com um prazo limite. Lança TimeoutException se a tarefa não terminar a tempo:
try {
Integer v = f.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
f.cancel(true); // give up; ask the task to stop
throw new ServiceUnavailableException("timed out");
}Esta é a forma certa para "estou chamando um backend que deve responder em N ms; caso contrário, falhe rapidamente." Sempre combine o catch com um cancel(true) — caso contrário, a tarefa continuará executando em segundo plano, usando uma thread cujo resultado você não se importa mais.
cancel(boolean)
Solicita que a tarefa pare:
boolean cancelled = f.cancel(true); // true = interrupt the running threadO argumento indica ao executor se deve interromper a thread do worker. Com true, o worker recebe uma InterruptedException de qualquer chamada bloqueante (sleep, wait, I/O); com false, o cancelamento não tem efeito se a tarefa já começou — apenas tarefas ainda não iniciadas são removidas da fila.
cancel é cooperativo. Uma tarefa que não verifica Thread.currentThread().isInterrupted() e não possui chamadas bloqueantes continuará executando até terminar. O cancelamento não é um interruptor de desligamento — é uma solicitação que a tarefa precisa honrar.
Exceções: a regra de encapsulamento
Tudo que o Callable lança é encapsulado em ExecutionException quando você chama get. A causa é o throwable original:
Future<Integer> f = pool.submit(() -> { throw new IOException("nope"); });
try {
f.get();
} catch (ExecutionException e) {
e.getCause(); // IOException("nope")
e.getCause() instanceof IOException; // true
}Note a assimetria: o Callable pode lançar uma exceção verificada (o throws Exception na sua assinatura), mas Future.get apenas declara ExecutionException. O encapsulamento é o que permite que uma única assinatura carregue todas as falhas possíveis.
A sobrecarga Runnable.submit — pool.submit(Runnable) — retorna um Future<?> cujo get() retorna null em caso de sucesso e ainda encapsula qualquer RuntimeException não capturada do Runnable. Essa é a forma padrão de descobrir que um runnable "fire and forget" realmente falhou.
Limitações do Future
Future é um canal unidirecional: você submete, aguarda e obtém o valor. Ele não compõe:
- Você não pode dizer "quando isso terminar, execute aquilo sobre o resultado."
- Você não pode dizer "quando qualquer um desses N terminar, faça X."
- Você não pode dizer "combine os resultados desses dois futures sem bloquear."
Para tudo isso você precisa do CompletableFuture (próximo capítulo). Future é a ferramenta certa quando:
- Você só quer um valor de volta de uma única tarefa.
- Você está consumindo uma API que retorna
Futures e não precisa compor. - O contrato mais simples é suficiente.
Para código moderno que faz muita composição assíncrona, você irá pular o Future e ir direto para o CompletableFuture — mas Future é o tipo que o executor service ainda retorna de submit, então você verá os dois.
FutureTask — a implementação por trás do submit
A classe que alimenta o submit. Você pode usá-la diretamente:
FutureTask<Integer> task = new FutureTask<>(() -> compute());
new Thread(task).start(); // FutureTask is a Runnable
Integer v = task.get();A maioria do código não constrói FutureTask diretamente; o framework executor faz isso por você. Mas é útil quando você precisa de um Future e um Runnable em um único objeto — por exemplo, para agendá-lo em algo diferente de um ExecutorService.
Um exemplo completo: submeter, definir timeout e propagar
O programa abaixo submete uma tarefa lenta, uma tarefa rápida e uma tarefa que falha; demonstra get, get(timeout), desencapsulamento de exceção e cancelamento.
O que extrair da execução:
- A seção 1 é a forma mais simples: submeta um
Callable, chameget, receba o valor.getbloqueou a thread principal pelos 50 ms que a tarefa levou. É tudo queFuturefaz na sua forma básica — um identificador tipado e bloqueante para um resultado que chega posteriormente. - A seção 2 mostrou a forma com timeout. A tarefa lenta teria executado por 500 ms;
get(100, MS)desistiu após 100 ms e lançouTimeoutException. Ocancel(true)subsequente interrompeu a thread em execução para que ela pudesse sair mais cedo. Sem o cancel, a tarefa teria continuado executando pelos 400 ms restantes — usando uma thread cujo resultado você não se importa mais. - A seção 3 mostrou o encapsulamento de exceção. O
CallablelançouIOException;get()a relançou dentro deExecutionException.e.getCause()devolveu a original. Este é o canal de falha universal da API — todo lançamento verificado ou não verificado do corpo aterrissa aqui. - A seção 4 mostrou o cancelamento de uma tarefa ainda não iniciada. Com as duas threads do pool ocupadas em
hog1ehog2, a tarefaqueuedestava na fila de trabalho;cancel(false)a removeu sem nunca executá-la. Chamarget()no future cancelado lançouCancellationException— um modo de falha diferente de "tarefa lançou" (que teria sidoExecutionException). - A seção 5 mostrou
invokeAny. A tarefa mais rápida (50 ms) venceu; as outras duas foram canceladas pelo executor.invokeAnyé a ferramenta certa para consultas redundantes — chame múltiplas fontes, use o primeiro sucesso, abandone o restante. É o bloco de construção por trás de padrões de requisições hedge em sistemas reais.
O que vem a seguir
O próximo capítulo, Java CompletableFuture, apresenta a API assíncrona combinável — thenApply, thenCompose, allOf, anyOf e as dezenas de combinadores que transformam o Future de um identificador de resultado único em um pipeline reativo completo.