Interface Runnable em Java
Defina unidades de trabalho para threads em Java com a interface funcional Runnable — a forma preferida para thread, executor e virtual thread.
Runnable é uma interface de um único método — possivelmente a mais importante em java.lang. Tudo que "roda em uma thread" em Java é, no fundo, um Runnable em algum lugar: o construtor de Thread aceita um, ExecutorService.execute aceita um, os shutdown hooks da JVM aceitam um. A razão pela qual o capítulo anterior recomendou "passe um Runnable ao construtor de Thread" em vez de "estenda Thread" é que Runnable separa o que executa de o que o executa. Essa separação é o que faz com que a mesma tarefa funcione em uma platform thread, um pool de threads ou uma virtual thread sem alterar o código.
A estrutura
Toda a definição cabe em três linhas:
@FunctionalInterface
public interface Runnable {
void run();
}É isso. Duas consequências decorrem dessas três linhas:
- É uma interface funcional. Qualquer lambda ou referência de método com assinatura sem argumentos e retorno void a implementa:
() -> System.out.println("hi"),this::flush,Foo::staticMethod. - Retorna void e não lança exceções verificadas. Esse é o limite do que você pode expressar. Se precisar de um resultado ou lançar algo verificado, você precisará de
Callable(um ou dois capítulos à frente).
Três formas de escrever um
// 1. Lambda — the modern default
Runnable r1 = () -> System.out.println("hello");
// 2. Method reference — when an existing method has the right signature
Runnable r2 = System.out::flush;
// 3. Anonymous class — pre-Java-8 form, occasionally useful when the body needs fields
Runnable r3 = new Runnable() {
@Override public void run() {
System.out.println("hello");
}
};Todas as três produzem um objeto do tipo Runnable. A forma lambda é preferida desde o Java 8; a forma de classe anônima só é útil quando você precisa de campos próprios (o que geralmente não acontece — capture variáveis locais em vez disso).
Como Runnable é utilizado
Três das principais APIs que recebem Runnable:
new Thread(runnable).start(); // platform thread, dedicated
executor.execute(runnable); // thread pool or virtual thread
Runtime.getRuntime().addShutdownHook(new Thread(runnable)); // JVM shutdownA mesma instância de Runnable funciona nos três contextos. Esse é o ponto central do design: o o quê (o trabalho) e o onde (a thread) são ortogonais. Você pode escrever código que realiza o trabalho e outra pessoa pode decidir em que thread executá-lo.
O contraste com a forma de subclasse de Thread torna isso concreto:
// Coupled: this work can only run on its own dedicated platform thread.
class ImageResizer extends Thread {
@Override public void run() { resize(); }
}
new ImageResizer().start();
// Decoupled: the same body runs anywhere.
Runnable resize = this::resize;
new Thread(resize).start(); // dedicated thread
executor.execute(resize); // pool
virtualExecutor.execute(resize); // virtual threadA forma desacoplada é por isso que o Java em produção está cheio de Runnable (e Callable) e quase nunca possui uma classe que estende Thread.
Variáveis capturadas devem ser efetivamente finais
Um lambda que se torna um Runnable pode ler variáveis locais do método que o envolve, mas apenas aquelas que o compilador pode provar que são efetivamente finais — atribuídas exatamente uma vez e nunca reatribuídas:
String name = "alice";
int n = 3;
Runnable r = () -> {
for (int i = 0; i < n; i++) {
System.out.println(name + " " + i);
}
};
// n = 4; // would break the lambda above — compile errorSe você precisar de estado mutável compartilhado, não pode usar uma variável local capturada — você precisa de um campo, um AtomicInteger, um slot de array ou outro objeto cujos internos sejam mutáveis. A restrição é intencional: lambdas capturam valores, não aliases, e proibir a reatribuição é a regra mais simples que torna isso consistente.
A solução mais comum é o array de um elemento:
int[] counter = {0};
Runnable r = () -> counter[0]++; // works; the array reference is final, the int inside isn'tMas para contadores compartilhados com segurança entre threads, um AtomicInteger é a escolha certa — veremos o porquê em alguns capítulos à frente.
Tratamento de exceções: nada para capturar, nada para recuperar
run() não lança exceções verificadas. Se seu worker pode falhar com uma exceção verificada, você deve capturá-la dentro de run():
Runnable parseFile = () -> {
try {
Files.readAllLines(path);
} catch (IOException e) {
log.error("parse failed", e); // you HAVE to handle it here
}
};Para exceções não verificadas, a situação é pior: nada no código chamador as captura. Se seu Runnable lançar NullPointerException em uma thread separada, a exceção vai para o handler de exceções não capturadas dessa thread e a thread morre. A thread principal não fica sabendo.
Duas formas de lidar com isso:
- Capture tudo dentro de
run()e registre você mesmo. Rudimentar, mas confiável. - Use
CallableeFuture.get(). OFuturerelança a exceção na thread que chamouget(). Isso é o que o framework de executors oferece.
Para trabalhos pontuais, a opção 1 é suficiente; para qualquer coisa que produza um resultado que o chamador precisa, a opção 2 é a resposta correta.
Runnable vs. Callable
Uma comparação lado a lado das duas interfaces de tarefa — você encontrará Callable corretamente mais adiante, mas o contraste é útil agora:
Runnable | Callable<V> | |
|---|---|---|
| Método | void run() | V call() throws Exception |
| Valor de retorno | Nenhum | Resultado tipado V |
| Exceções verificadas | Não pode lançar | Pode lançar qualquer Exception |
| Aceito por | new Thread, Executor.execute, shutdown hooks | ExecutorService.submit |
| Handle de resultado | Nenhum (fire and forget) | Future<V> |
Sempre que precisar de um valor de retorno ou da capacidade de lançar exceções verificadas, mude para Callable. Para trabalho de efeito colateral puro — flushing, logging, agendamento — Runnable é a ferramenta mais leve.
Um exemplo prático: mesmo Runnable, três executores
O programa abaixo define um Runnable que realiza uma pequena parte do trabalho e, em seguida, executa a mesma instância em (a) uma nova platform thread, (b) um ExecutorService e (c) a thread chamadora via .run() direto. O mesmo corpo é executado nos três contextos; a única coisa que muda é o executor.
O que observar na execução:
- Os três primeiros blocos executaram a mesma instância
greetem três executores diferentes — chamada direta, thread dedicada, pool de threads. O nome da thread impresso porgreetmudou a cada vez:main,dedicated-worker,pool-1-thread-1. Esse é o principal motivo para preferirRunnableem vez de subclassificarThread: o trabalho é reutilizável, o executor é substituível. - A
RuntimeExceptionda threadcrashynão matou omain. Ela morreu em sua própria thread e o handler de exceções não capturadas a relatou. Sem um handler, a JVM imprime um stack trace no stderr e o restante do programa continua executando — o que muitas vezes é pior, porque o trabalho que a thread deveria realizar silenciosamente não aconteceu. - O lambda
shoutcapturounameendas variáveis locais demain. Elas são efetivamente finais — atribuídas uma vez, nunca reatribuídas. Adicionen = 4;em qualquer lugar após o lambda ser definido e o arquivo para de compilar. Essa restrição é o que torna a captura em lambda segura entre threads. - O exemplo
bumpusouAtomicIntegerporque duas threads estavam incrementando o mesmo contador. Com um campointsimples, o valor final estaria em algum lugar entre1000e2000— atualizações perdidas pori++não atômico.incrementAndGet()é a correção mais simples e voltaremos a isso no capítulo sobre atomics. - A única instância compartilhada de
Runnablefoi passada paranew Thread(bump, "a")enew Thread(bump, "b")— o mesmo lambda rodou em duas threads simultaneamente. O lambda não tem campos próprios; tudo que ele acessa vive fora dele. Esse é o formato de todoRunnableparalelo seguro: mantenha o mínimo possível de estado interno e empurre o estado para um objeto thread-safe que as threads compartilham.
O que vem a seguir
O próximo capítulo, Ciclo de Vida de Threads em Java, percorre os seis valores de Thread.State — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — e mostra como ler um thread dump que os expõe.