W3docs

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:

  1. Retorna V (o parâmetro de tipo).
  2. 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 SQLException

Callable é 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 42

get() 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 chamou cancel() 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 thread

O 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.submitpool.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.

java— editable, runs on the server

O que extrair da execução:

  • A seção 1 é a forma mais simples: submeta um Callable, chame get, receba o valor. get bloqueou a thread principal pelos 50 ms que a tarefa levou. É tudo que Future faz 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çou TimeoutException. O cancel(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 Callable lançou IOException; get() a relançou dentro de ExecutionException. 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 hog1 e hog2, a tarefa queued estava na fila de trabalho; cancel(false) a removeu sem nunca executá-la. Chamar get() no future cancelado lançou CancellationException — um modo de falha diferente de "tarefa lançou" (que teria sido ExecutionException).
  • 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.

Prática

Prática
Você chama `future.get()` e a tarefa lançou `SQLException` do seu método `call()`. Qual exceção `get()` lança?
Você chama `future.get()` e a tarefa lançou `SQLException` do seu método `call()`. Qual exceção `get()` lança?
Was this page helpful?