Task Cancellation: Interrupção Feita Corretamente
Objective
Thread.interrupt() não interrompe nada. Ele apenas vira um boolean por thread — o interrupt status — e nada mais; se essa flag chega a mudar o comportamento da thread alvo depende inteiramente de o código dessa thread se dar ao trabalho de checá-la, seja explicitamente via Thread.isInterrupted(), seja implicitamente por estar bloqueada dentro de um método que já a checa por você (Thread.sleep(), Object.wait(), BlockingQueue.take()/put(), e a maioria das outras chamadas bloqueantes de java.util.concurrent, que respondem limpando a flag e lançando InterruptedException). Escrever código cancelável corretamente significa tratar essa flag da forma certa: checá-la em pontos seguros e — de forma crítica — nunca capturar InterruptedException e descartá-la, já que fazer isso torna a thread silenciosamente não interruptível pelo resto de sua vida.
Use Cases
- Escrever um loop de longa duração (uma worker thread, um poller em background, um job em lote) que precisa parar prontamente quando solicitado, sem corromper o que estava em andamento.
- Revisar um bloco
catch (InterruptedException e) { }durante um code review — reconhecendo-o como um bug, não uma questão de estilo. - Decidir se o código que você está escrevendo pode chamar
interrupt()em uma thread, ou se precisa em vez disso cancelar através do dono dessa thread (tipicamente umFuturevindo de umExecutorService). - Combinar um timeout com cancelamento: dar a uma task um prazo e cancelá-la se ela ultrapassar esse prazo.
Deep Dive
1. Uma flag de status, não um sinal de parada
interrupt() define um boolean na Thread alvo. Esse é o mecanismo inteiro. O que acontece depois depende de onde essa thread está:
javaThread worker = new Thread(() -> System.out.println("running"));
worker.start();
worker.interrupt(); // sets the flag; does not pause, kill, or redirect anythingSe a thread alvo está fazendo trabalho comum, ligado à CPU, nada acontece até que o próprio código da thread decida olhar para a flag. Um loop corretamente ciente de interrupção a checa entre unidades de trabalho, em um ponto onde parar é seguro:
javaclass Worker implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
doUnitOfWork(); // one safe, self-contained chunk of work
}
// loop exits on its own terms — never mid-update
}
}Se a thread alvo está, em vez disso, bloqueada dentro de um método interruptível — Thread.sleep(), Object.wait(), Thread.join(), ou uma chamada bloqueante como BlockingQueue.take()/put() — a JVM percebe a interrupção por você: ela limpa a flag e lança InterruptedException ali mesmo, acordando a thread imediatamente em vez de fazê-la esperar até a próxima sondagem explícita:
javaclass Consumer implements Runnable {
private final BlockingQueue<String> queue;
Consumer(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
String item = queue.take(); // blocks — and IS interruptible
process(item);
}
} catch (InterruptedException e) {
// take() already cleared the interrupt status when it threw it;
// see section 2 for what to do here instead of nothing.
Thread.currentThread().interrupt();
}
}
}Note a checagem explícita de isInterrupted() mesmo com take() já sendo interruptível: se a flag foi definida antes de a thread chegar a chamar take() de novo, a checagem a captura imediatamente, em vez de iniciar outra espera bloqueante primeiro.
2. O bug de InterruptedException engolida
O erro mais comum nessa área é capturar InterruptedException e não fazer nada com ela:
java// WRONG — do not do this
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
queue.put(nextItem());
}
} catch (InterruptedException e) {
// swallowed: nothing here restores the flag or tells anyone
}
}Lembre-se: um método bloqueante limpa o interrupt status no exato momento em que lança InterruptedException para sinalizá-lo. Se o bloco catch não faz nada, essa limpeza é permanente — todo e qualquer traço de que essa thread já foi solicitada a parar desaparece. Qualquer código mais acima na call stack que depois checar isInterrupted() verá false, como se o cancelamento nunca tivesse sido solicitado. A thread agora é efetivamente não interruptível pelo resto de sua vida, mesmo que o contrato da API prometesse que ela poderia ser interrompida.
Existem exatamente duas respostas corretas. A melhor, quando a assinatura do seu método permite, é propagar a exceção e deixar o chamador lidar com ela:
java// RIGHT — propagate
public String getNextTask(BlockingQueue<String> queue) throws InterruptedException {
return queue.take(); // let InterruptedException bubble up unmodified
}Quando você não pode propagar — mais comumente porque está dentro de Runnable.run(), que não tem uma cláusula throws para adicionar InterruptedException — restaure o status em vez de descartá-lo, chamando Thread.currentThread().interrupt() antes de retornar:
java// RIGHT — restore, when propagation isn't possible
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
queue.put(nextItem());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // put the flag back before returning
}
}Isso devolve a flag ao lugar, de forma que qualquer código que checar isInterrupted() em seguida — o thread pool dono dessa thread, um chamador mais acima na pilha, outra iteração de loop — ainda descubra que um pedido de cancelamento aconteceu, mesmo que este método específico já tenha feito sua própria limpeza local.
3. Política de interrupção: você não é dono da thread
Uma política de interrupção é a resposta para "o que essa thread faz quando é interrompida?" — geralmente "parar assim que for prático, limpar, sair". A regra importante é quem define essa política: quem é dono da thread, não quem por acaso está rodando nela. Código submetido a um thread pool é um convidado numa thread que não possui; o ExecutorService é o dono, e ele já se comprometeu a tratar interrupção como "cancelar a task atual". Inventar um segundo mecanismo de cancelamento privado em cima dessa thread emprestada cria dois sinais competindo em vez de um só:
java// Avoid on a pooled task — this is a private cancellation channel
// competing with the pool's own interruption-based one.
class MyTask implements Runnable {
private volatile boolean stop;
void requestStop() {
stop = true;
}
@Override
public void run() {
while (!stop) {
doWork();
}
}
}java// Prefer this on a pooled task: rely on the interruption
// mechanism the ExecutorService already implements.
class MyTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
doWork();
}
}
}
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<?> handle = pool.submit(new MyTask());
handle.cancel(true); // the pool's interruption policy takes it from hereO outro lado da mesma regra: também não chame interrupt() diretamente em uma thread que você não possui — você não sabe qual task está rodando nela no momento nem qual é a política de interrupção dessa task. Cancele através da abstração que o dono lhe deu (um Future, um método cancel()/shutdown()), não invadindo a thread diretamente.
4. Future.cancel(boolean mayInterruptIfRunning)
Para uma task submetida a um ExecutorService, Future.cancel(boolean) é a API de cancelamento padrão, de nível mais alto — é o que a seção 3 já estava chamando com handle.cancel(true). Quando mayInterruptIfRunning é true e a task está rodando no momento, o executor chama interrupt() na thread que a executa internamente; quando false, a task só é impedida de começar se ainda não tiver começado. Combinado com um timeout, isso dá a você "rode isso, mas desista e cancele se demorar demais":
javaExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<String> future = executor.submit(this::slowComputation);
try {
String result = future.get(2, TimeUnit.SECONDS);
System.out.println(result);
} catch (TimeoutException e) {
future.cancel(true); // interrupt the task if it's still running
} catch (ExecutionException e) {
throw new RuntimeException(e.getCause());
} finally {
executor.close();
}Chamar cancel(true) é inofensivo mesmo que a task já tenha terminado — cancelar uma task já concluída simplesmente não tem efeito nenhum — motivo pelo qual é seguro chamá-lo incondicionalmente em um bloco finally sempre que o resultado deixou de importar, seja porque get() deu timeout, lançou exceção, ou você simplesmente parou de se importar com a resposta.
Trade-offs
- Interrupção é consultiva, não preemptiva — uma thread que nunca checa
isInterrupted()e nunca chama um método bloqueante interruptível simplesmente nunca percebe que foi interrompida; não há como forçá-la a parar de fora.
javaThread t = new Thread(() -> {
long x = 0;
while (true) { x++; } // no isInterrupted() check, no blocking call
});
t.start();
t.interrupt(); // flag is set; the loop above never looks at it- Restaurar vs. propagar é uma decisão real de design, não boilerplate — propagar empurra
InterruptedException(e a cláusulathrows) para todo chamador acima na cadeia; restaurar mantém a assinatura do seu método limpa, mas significa que a flag fica sem checagem até que algum código que a sonda mais adiante finalmente o faça. - Algumas chamadas bloqueantes simplesmente não respondem a interrupção — I/O síncrono clássico de socket (
InputStream.read()/OutputStream.write()em umSocketcomum) ignorainterrupt()completamente; a solução costumeira é fechar o socket subjacente para que a chamada bloqueada falhe com uma exceção em vez disso, e não há uma correção geral limpa além dessa.
javaSocket socket = new Socket(host, port);
Thread reader = new Thread(() -> {
try {
socket.getInputStream().read(); // blocks; interrupt() has no effect here
} catch (IOException e) {
// thrown once the socket is closed from outside
}
});
reader.start();
reader.interrupt(); // does nothing to unblock read()
socket.close(); // this is what actually unblocks itcancel(true)só garante entrega, não conformidade — ele diz a você que a interrupção foi entregue com sucesso à thread em execução, não que a task percebeu e de fato parou; uma task que ignora seu próprio interrupt status continua rodando não importa quantas vezescancel()seja chamado.