1. Thread.interrupt() e cancelamento cooperativo
Em aplicações reais, as tarefas podem ser longas e às vezes até “travar” — ao trabalhar com rede, arquivos ou serviços externos. O usuário pode cancelar a operação, o servidor pode interromper o processamento da requisição ou simplesmente o timeout geral pode expirar. Se você não souber cancelar tarefas corretamente, o aplicativo ficará travado, gastará recursos à toa e reagirá mal a eventos externos.
Ideia‑chave: o cancelamento deve ser cooperativo — a própria tarefa deve verificar se foi solicitada a finalizar e liberar recursos corretamente.
Como funciona Thread.interrupt()
Cada thread tem um sinalizador de interrupção. Quando você chama thread.interrupt(), esse sinalizador é definido como true. A thread não é “morta” automaticamente; ela deve verificar seu próprio status e encerrar-se: chamar periodicamente Thread.currentThread().isInterrupted() e sair corretamente.
Exemplo:
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Trabalhando...
try {
Thread.sleep(100); // Pode ser interrompido
} catch (InterruptedException e) {
// O sinalizador é limpo, mas podemos nos interromper novamente
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Thread finalizada por interrupção.");
});
worker.start();
// ... mais tarde
worker.interrupt();
Onde o sinalizador funciona automaticamente?
- Métodos que podem bloquear (sleep, wait, join, operações de estruturas bloqueantes) lançam InterruptedException ao serem interrompidos.
- Nos demais casos (por exemplo, em um laço computacional) é preciso verificar manualmente isInterrupted().
Padrão “defina o sinalizador — e saia rapidamente”
- No código chamador: thread.interrupt()
- Na tarefa: verificar periodicamente Thread.currentThread().isInterrupted()
- Se necessário — liberar recursos corretamente e encerrar.
Erro típico: esperar que interrupt() “mate” a thread instantaneamente. Não — é apenas um sinal; a tarefa deve reagir por conta própria.
2. Future.cancel(), CancellationException e cancelamento de tarefas
Como funciona Future.cancel
Quando você executa uma tarefa via ExecutorService.submit(), recebe um objeto Future. Ele possui o método cancel(boolean mayInterruptIfRunning):
- Se a tarefa ainda não começou — ela não será iniciada.
- Se a tarefa já estiver em execução e mayInterruptIfRunning == true — será chamado interrupt() na thread que executa a tarefa.
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Trabalho demorado
}
System.out.println("Tarefa finalizada por cancelamento.");
});
// ... mais tarde
future.cancel(true); // Solicitar cancelamento da tarefa
O que realmente acontece com a tarefa
O cancelamento via Future não é um botão mágico de “matar thread”; é, na prática, uma forma educada de Thread.interrupt(). Se a tarefa verifica o sinalizador de interrupção corretamente — ela termina de forma adequada. Caso contrário — continuará trabalhando até seu término natural.
Se você chamar future.get() após o cancelamento, receberá CancellationException — um lembrete de que a tarefa foi cancelada.
3. CompletableFuture: cancelamento, timeouts e cadeias
Cancelamento em CompletableFuture
CompletableFuture também possui cancel(boolean). Se a tarefa ainda não terminou, ela será cancelada e todos os manipuladores subsequentes (thenApply, thenAccept etc.) não serão chamados.
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Trabalhando...
}
System.out.println("CF finalizado por cancelamento.");
});
// ... mais tarde
cf.cancel(true);
Timeouts: orTimeout e completeOnTimeout
- orTimeout(timeout, unit) — finaliza o CompletableFuture com TimeoutException se ele não terminar a tempo.
- completeOnTimeout(value, timeout, unit) — finaliza com o valor especificado, se não terminar a tempo.
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(5000); } catch (InterruptedException e) {}
return "OK";
});
cf.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> "TIMEOUT")
.thenAccept(System.out::println); // Em 2 segundos: "TIMEOUT"
Propagação do cancelamento em cadeias
Se você cancelar o CompletableFuture “superior”, todas as etapas subsequentes na cadeia não serão chamadas. Mas ao usar thenCompose para iniciar operações assíncronas internas, o cancelamento não é propagado automaticamente “para cima” — é preciso projetá-lo explicitamente (verificar status, cancelar tarefas filhas, usar um deadline comum).
Cuidado com thenCompose e Executor personalizado! Garanta que as tarefas internas saibam reagir a interrupção/cancelamento e/ou recebam um timeout comum.
4. StructuredTaskScope: cancelamento de grupo de tarefas
Concorrência estruturada e cancelamento
StructuredTaskScope (Java 21+) permite iniciar um grupo de tarefas e gerenciar seu ciclo de vida como um todo. Se uma das tarefas terminar com erro ou o timeout expirar — as demais tarefas serão automaticamente canceladas.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
scope.join(); // aguardamos a conclusão de todas as tarefas
scope.throwIfFailed(); // se alguma falhou — lançamos exceção
String result = f1.resultNow() + f2.resultNow();
System.out.println(result);
}
- Se qualquer tarefa terminar com erro — o scope cancela todas as demais tarefas.
- Se o timeout expirar (via scope.joinUntil(deadline)) — o scope cancela todas as tarefas.
Políticas de término
- ShutdownOnFailure — cancela todas as tarefas na primeira falha.
- ShutdownOnSuccess — cancela as demais tarefas assim que uma terminar com sucesso.
5. Prática: cancelamento seguro de operações longas
Exemplo: cancelamento de IO bloqueante
Se a tarefa está bloqueada lendo de um arquivo ou da rede, interromper a thread nem sempre ajuda — algumas operações de IO não reagem a interrupt. Em APIs modernas (NIO, AsynchronousFileChannel) a interrupção é melhor suportada, mas ainda não em todos os lugares.
Recomendações:
- Use IO não bloqueante se precisar de cancelamento.
- Para IO bloqueante — defina timeouts no nível da API (por exemplo, Socket.setSoTimeout).
- Para tarefas assíncronas — use Future.cancel e reaja corretamente à interrupção.
Exemplo: cancelamento de espera em fila/barreira
Muitos sincronizadores (BlockingQueue.take(), CountDownLatch.await(), CyclicBarrier.await()) lançam InterruptedException quando interrompidos. No tratador, capture a exceção, restaure o sinalizador se necessário e finalize a tarefa corretamente.
6. Padrão “time‑budget”: deadline comum para um grupo de operações
Em aplicativos complexos, muitas vezes é necessário definir um timeout comum para executar um grupo de operações. Por exemplo, se o usuário aguarda a resposta por no máximo 2 segundos, e internamente é preciso fazer 3 requisições de rede — todas devem respeitar o mesmo deadline.
Como propagar o deadline para baixo na pilha?
- Passe um objeto de deadline (por exemplo, Instant deadline) para todos os métodos potencialmente bloqueantes.
- Em cada método, calcule o tempo restante: Duration.between(Instant.now(), deadline).
- Use esse tempo para timeouts em operações bloqueantes (await(timeout), poll(timeout), orTimeout(timeout)).
Instant deadline = Instant.now().plusSeconds(2);
void doWork(Instant deadline) throws TimeoutException, InterruptedException {
Duration left = Duration.between(Instant.now(), deadline);
if (left.isNegative() || left.isZero()) throw new TimeoutException();
// Usamos left para o timeout
queue.poll(left.toMillis(), TimeUnit.MILLISECONDS);
}
Scoped Values / contexto
No Java 21+ é possível usar Scoped Values para passar o deadline pela pilha de chamadas, sem ter que passá‑lo explicitamente para cada método.
7. Structured Concurrency: cancelar todo o scope em caso de falha/timeout
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
boolean completed = scope.joinUntil(Instant.now().plusSeconds(2));
if (!completed) {
scope.shutdown();
throw new TimeoutException("O deadline expirou!");
}
scope.throwIfFailed();
// ...
}
- Se o deadline expirar — o scope cancela todas as tarefas.
- Se uma tarefa falhar — as demais são canceladas automaticamente.
8. Erros comuns ao lidar com cancelamento e timeouts
Erro nº 1: Esperar que interrupt() termine a thread instantaneamente. Na verdade, é apenas um sinal — a tarefa deve verificar o status e encerrar corretamente.
Erro nº 2: Não verificar isInterrupted() em laços longos. Se não verificar o sinalizador de interrupção, a tarefa continuará executando para sempre, mesmo que tenha sido solicitada a finalizar.
Erro nº 3: Future.cancel() não leva ao cancelamento se a tarefa não reage a interrupt. Se a tarefa for “surda”, cancel() não ajudará.
Erro nº 4: Não propagar timeouts para baixo na pilha. Se você não passar o deadline para todos os métodos, uma operação interna pode “travar” por mais tempo do que o necessário.
Erro nº 5: Em cadeias de thenCompose de CompletableFuture o cancelamento não é propagado automaticamente. Se cancelar o future “de cima”, tarefas internas podem continuar executando — trate o cancelamento explicitamente.
Erro nº 6: StructuredTaskScope não é fechado (sem try‑with‑resources). Se o scope não for fechado, tarefas filhas podem ficar “penduradas”.
GO TO FULL VERSION