W3docs

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 shutdown

A 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 thread

A 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 error

Se 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't

Mas 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:

  1. Capture tudo dentro de run() e registre você mesmo. Rudimentar, mas confiável.
  2. Use Callable e Future.get(). O Future relança a exceção na thread que chamou get(). 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:

RunnableCallable<V>
Métodovoid run()V call() throws Exception
Valor de retornoNenhumResultado tipado V
Exceções verificadasNão pode lançarPode lançar qualquer Exception
Aceito pornew Thread, Executor.execute, shutdown hooksExecutorService.submit
Handle de resultadoNenhum (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.

java— editable, runs on the server

O que observar na execução:

  • Os três primeiros blocos executaram a mesma instância greet em três executores diferentes — chamada direta, thread dedicada, pool de threads. O nome da thread impresso por greet mudou a cada vez: main, dedicated-worker, pool-1-thread-1. Esse é o principal motivo para preferir Runnable em vez de subclassificar Thread: o trabalho é reutilizável, o executor é substituível.
  • A RuntimeException da thread crashy não matou o main. 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 shout capturou name e n das variáveis locais de main. Elas são efetivamente finais — atribuídas uma vez, nunca reatribuídas. Adicione n = 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 bump usou AtomicInteger porque duas threads estavam incrementando o mesmo contador. Com um campo int simples, o valor final estaria em algum lugar entre 1000 e 2000 — atualizações perdidas por i++ 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 Runnable foi passada para new Thread(bump, "a") e new 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 todo Runnable paralelo 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.StateNEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — e mostra como ler um thread dump que os expõe.

Prática

Prática
Qual afirmação sobre `Runnable` é verdadeira?
Qual afirmação sobre `Runnable` é verdadeira?
Was this page helpful?