Java Threads
Até aqui, todo programa fez uma coisa de cada vez: uma linha depois da outra. Uma thread (em inglês, "fio" ou "linha", como o fio de uma conversa) é uma linha de execução independente. Com várias threads, o programa faz várias coisas ao mesmo tempo: baixar um arquivo enquanto mostra uma barra de progresso, atender vários clientes de uma vez. O preço é que a ordem das coisas deixa de ser previsível.
- O que é uma thread e o que é a thread
main. - Criar e iniciar uma thread com
new Thread(() -> …).start(). - Por que a saída sai misturada e muda a cada execução (com duas execuções reais).
- Esperar uma thread com
join, pausar comThread.sleep, dar nomes às threads. - As formas de escrever a tarefa (
Runnable, lambda,extends Thread), o erro de chamarrun()no lugar destart()e o perigo de duas threads mexerem na mesma variável.
Esta aula usa lambdas e exceções checked. É a primeira do grupo Concorrência; as próximas são ExecutorService, Threads Virtuais e synchronized e Locks. Os exemplos rodaram no JDK 25 da máquina da Ana, na pasta ~/java.
A ideia antes do código
Imagine a cozinha da lanchonete da Ana. Com uma cozinheira, os pedidos saem um depois do outro: ela frita a batata, depois monta o lanche, depois faz o suco. Com duas cozinheiras, enquanto uma frita a batata a outra monta o lanche. Os pedidos saem mais rápido, mas agora surgem perguntas novas:
- Quem termina primeiro? Depende: de quem pegou a frigideira antes, de quem foi atender o telefone… Cada dia é diferente.
- Como a Ana sabe que as duas terminaram antes de entregar a bandeja? Ela precisa esperar.
- E se as duas pegarem o mesmo pote de sal ao mesmo tempo? Confusão.
Cada cozinheira é uma thread. Quem decide quem trabalha em cada momento é o escalonador (scheduler) do sistema operacional, que distribui os núcleos do processador entre as threads, trocando entre elas milhares de vezes por segundo. Você não controla essa escala. Essas três perguntas são o assunto desta aula e das próximas.
1. Você sempre esteve numa thread: a main
Quando você roda java Main, a JVM cria uma thread chamada main e começa por ela o método main. O método estático Thread.currentThread() ("thread atual") diz em qual thread uma linha está rodando:
public class Main {
public static void main(String[] args) {
System.out.println("Olá da thread " + Thread.currentThread().getName());
}
}
$ javac Main.java && java Main Olá da thread main
2. Criando outras threads
A classe Thread representa uma thread. Você entrega a ela a tarefa (o código que deve rodar) e manda começar com start(). A Ana e o Bruno vão embalar cinco pedidos cada um, ao mesmo tempo:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread ana = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Ana embalou o pedido " + i);
}
});
Thread bruno = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Bruno embalou o pedido " + i);
}
});
ana.start();
bruno.start();
ana.join();
bruno.join();
System.out.println("Todos os pedidos embalados.");
}
}
- new Thread(
- Cria um objeto
Thread. Ele ainda não está rodando: é a cozinheira contratada, mas parada. - () -> { … }
- A tarefa, escrita como uma lambda: sem parâmetros, com o código entre chaves.
- ana.start()
- "Começar": cria a linha de execução de verdade e roda a tarefa nela. O
start()volta na hora; amainsegue para a próxima linha sem esperar. - ana.join()
- "Juntar-se": a
mainpara e espera a threadanaterminar. - throws InterruptedException
- O
joinpode lançar essa exceção checked (veja a seção 4); aqui ela é só repassada.
A mesma saída? Não.
A Ana compilou uma vez e rodou o programa duas vezes seguidas, sem mudar nada:
$ java Main Ana embalou o pedido 1 Bruno embalou o pedido 1 Ana embalou o pedido 2 Ana embalou o pedido 3 Bruno embalou o pedido 2 Ana embalou o pedido 4 Ana embalou o pedido 5 Bruno embalou o pedido 3 Bruno embalou o pedido 4 Bruno embalou o pedido 5 Todos os pedidos embalados. $ java Main Bruno embalou o pedido 1 Bruno embalou o pedido 2 Bruno embalou o pedido 3 Bruno embalou o pedido 4 Bruno embalou o pedido 5 Ana embalou o pedido 1 Ana embalou o pedido 2 Ana embalou o pedido 3 Ana embalou o pedido 4 Ana embalou o pedido 5 Todos os pedidos embalados.
Na primeira vez, as linhas saíram intercaladas (misturadas). Na segunda, o Bruno fez tudo antes de a Ana começar, mesmo tendo sido iniciado depois. Nas suas execuções, a ordem vai ser outra. Isso não é um defeito: é a natureza das threads. O escalonador decide quem roda em cada instante, e essa decisão depende de tudo o que está acontecendo no computador.
Mas repare no que não muda:
- Dentro de cada thread, a ordem é respeitada: a Ana sempre embala o 1 antes do 2.
Todos os pedidos embalados.é sempre a última linha, graças aos doisjoin.
O simulador do guia não tem threads de verdade: tudo roda numa thread só, de forma determinística (sempre igual), para que as missões possam ser conferidas. Lá, o start() roda a tarefa inteira na hora, antes de voltar, e o Thread.sleep não espera de verdade (veja O Editor do Guia). A saída dele é uma das ordens possíveis, nunca a mistura. Por isso as saídas desta aula, nos blocos de terminal, são do JDK real, e as missões só conferem o que é garantido: que cada linha aparece, e o que vem depois de um join.
Sem join, a main não espera
public class Main {
public static void main(String[] args) {
Thread entrega = new Thread(() -> {
System.out.println("Entrega saiu");
System.out.println("Entrega chegou");
});
entrega.start();
System.out.println("Fim do main");
}
}
$ javac Main.java && java Main Fim do main Entrega saiu Entrega chegou
A main chegou ao fim antes de a entrega começar. Mesmo assim o programa esperou a entrega: a JVM só termina quando todas as threads comuns acabam. Mas se a main fosse usar o resultado da entrega, ela o usaria antes de ele existir. Sempre que precisar do trabalho de uma thread, chame join().
Missão: crie as threads ana e bruno (cada uma mostra Ana terminou / Bruno terminou), inicie as duas com start(), espere as duas com join() e só então mostre Fim do expediente.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread ana = new Thread(() -> System.out.println("Ana terminou"));
// crie a thread bruno
// inicie as duas e espere as duas
System.out.println("Fim do expediente");
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread ana = new Thread(() -> System.out.println("Ana terminou"));
Thread bruno = new Thread(() -> System.out.println("Bruno terminou"));
ana.start();
bruno.start();
ana.join();
bruno.join();
System.out.println("Fim do expediente");
}
}
3. Nomes e Thread.sleep
O segundo argumento do construtor dá um nome à thread (sem ele, o Java inventa Thread-0, Thread-1…). E Thread.sleep(ms) ("dormir") pausa a thread atual pelo número de milissegundos indicado (1000 ms = 1 segundo), sem gastar processador:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread forno = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": bolo no forno");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
System.out.println("Alguém interrompeu o forno!");
return;
}
System.out.println(Thread.currentThread().getName() + ": bolo pronto");
}, "forno");
long inicio = System.currentTimeMillis();
forno.start();
System.out.println("main: enquanto isso, lavo a louça");
forno.join();
long fim = System.currentTimeMillis();
System.out.println("main: esperei o forno; levou cerca de " + (fim - inicio) / 100 * 100 + " ms");
}
}
$ javac Main.java && java Main main: enquanto isso, lavo a louça forno: bolo no forno forno: bolo pronto main: esperei o forno; levou cerca de 1500 ms
| Trecho | O que faz |
|---|---|
new Thread(() -> { … }, "forno") | Tarefa + nome da thread. |
Thread.currentThread().getName() | Dentro da tarefa, dá forno; na main, dá main. |
Thread.sleep(1500) | A thread forno dorme 1,5 segundo. A main continua livre (lavou a louça). |
catch (InterruptedException e) | Se outra thread "acordar" o forno no meio do sono (com forno.interrupt()), cai aqui. |
System.currentTimeMillis() | A hora atual em milissegundos; a diferença mede quanto tempo passou. O / 100 * 100 arredonda para baixo, de 100 em 100. |
4. Por que InterruptedException?
sleep e join são bloqueantes: a thread fica parada esperando. O Java permite que outra thread peça "pare de esperar" (interrupt()), e a espera termina com a exceção InterruptedException. Como ela é checked, o compilador obriga você a decidir o que fazer:
public class Main {
public static void main(String[] args) {
System.out.println("Esperando...");
Thread.sleep(1000);
System.out.println("Pronto");
}
}
$ javac Main.java
Main.java:4: error: unreported exception InterruptedException; must be caught or declared to be thrown
Thread.sleep(1000);
^
1 error
Duas saídas: declarar throws InterruptedException no método (bom no main de exemplos) ou usar try/catch (obrigatório dentro de uma lambda de Runnable, porque o método run() não pode declarar exceções checked).
5. Formas de escrever a tarefa
A tarefa de uma thread é um Runnable ("executável"): uma interface funcional com um único método, void run(). A lambda que você viu é um Runnable. Há outras formas:
class Contagem implements Runnable {
private final String fruta;
Contagem(String fruta) {
this.fruta = fruta;
}
@Override
public void run() {
System.out.println("Contando " + fruta + " na thread " + Thread.currentThread().getName());
}
}
class Entregador extends Thread {
Entregador(String nome) {
super(nome);
}
@Override
public void run() {
System.out.println(getName() + " saiu para entregar");
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Contagem("maçãs"), "contador-1");
Thread t2 = new Entregador("entregador-1");
Thread t3 = Thread.ofPlatform().name("caixa").start(() ->
System.out.println("Abrindo o caixa na thread " + Thread.currentThread().getName()));
t1.start();
t2.start();
t1.join();
t2.join();
t3.join();
}
}
$ javac Main.java && java Main entregador-1 saiu para entregar Abrindo o caixa na thread caixa Contando maçãs na thread contador-1
| Forma | Quando usar |
|---|---|
new Thread(() -> …) | Tarefa curta. A mais comum. |
class X implements Runnable + new Thread(new X(…)) | Tarefa maior, com dados próprios (a fruta). A classe pode herdar de outra. |
class X extends Thread | Forma antiga. Evite: mistura "o que fazer" com "quem faz", e gasta a única herança da classe. |
Thread.ofPlatform().name("…").start(tarefa) Java 21+ | Construtor "fluente": cria, nomeia e já inicia. Tem um irmão, Thread.ofVirtual(), assunto de Threads Virtuais. |
ExecutorService | Muitas tarefas: em vez de criar threads à mão, você entrega tarefas a um "time" de threads. É o que se usa em programas de verdade (próxima aula). |
6. O perigo: duas threads, uma variável
Duas threads que só leem dados não atrapalham uma à outra. O problema começa quando elas alteram a mesma variável. Os dois caixas da loja registram 100 000 vendas cada um no mesmo contador:
public class Main {
static int vendas = 0;
public static void main(String[] args) throws InterruptedException {
Runnable vender = () -> {
for (int i = 0; i < 100_000; i++) {
vendas++;
}
};
Thread caixa1 = new Thread(vender);
Thread caixa2 = new Thread(vender);
caixa1.start();
caixa2.start();
caixa1.join();
caixa2.join();
System.out.println("Vendas registradas: " + vendas + " (esperado: 200000)");
}
}
$ java Main Vendas registradas: 127820 (esperado: 200000) $ java Main Vendas registradas: 113625 (esperado: 200000) $ java Main Vendas registradas: 106254 (esperado: 200000)
Dezenas de milhares de vendas sumiram, e um número diferente a cada vez. O vendas++ parece um passo só, mas são três: ler o valor, somar 1, gravar. Se as duas threads leem o mesmo valor antes de qualquer uma gravar, uma soma se perde. Isso se chama condição de corrida, e a aula synchronized e Locks é inteira sobre como evitá-la.
Quando dá errado
Chamar run() em vez de start()
public class Main {
public static void main(String[] args) {
Runnable tarefa = () -> System.out.println("Rodando na thread: " + Thread.currentThread().getName());
Thread t = new Thread(tarefa, "ajudante");
t.run();
t.start();
}
}
$ javac Main.java && java Main Rodando na thread: main Rodando na thread: ajudante
O que aconteceu: t.run() é uma chamada de método comum: a tarefa rodou na própria main, sem thread nova nenhuma. Só o start() cria a linha de execução. O compilador não reclama, porque run() é um método público válido. Conserto: para rodar em paralelo, sempre start().
Iniciar a mesma thread duas vezes
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> System.out.println("Oi"));
t.start();
t.start();
}
}
$ javac Main.java && java Main Oi Exception in thread "main" java.lang.IllegalThreadStateException at java.base/java.lang.Thread.start(Thread.java:1416) at Main.main(Main.java:5)
O que quer dizer: "estado de thread ilegal". Uma thread só pode ser iniciada uma vez; depois que termina, não volta. Conserto: crie um new Thread(…) novo para cada execução, ou use um ExecutorService, que reaproveita threads.
unreported exception InterruptedException
sleep ou join sem tratar a exceção (seção 4). Conserto: throws InterruptedException no método, ou try/catch dentro da lambda.
O resultado muda a cada execução
Se a ordem das linhas muda, é normal (seção 2). Se um número muda, como o contador de vendas, há duas threads alterando a mesma variável sem proteção (seção 6). Veja synchronized e Locks.
Referência
| Código | O que faz |
|---|---|
new Thread(tarefa) / new Thread(tarefa, "nome") | Cria a thread (parada) |
t.start() | Inicia; volta na hora |
t.join() | Espera t terminar |
Thread.sleep(ms) | Pausa a thread atual |
Thread.currentThread() | A thread que está rodando esta linha |
t.getName() / t.setName("…") | Nome da thread |
t.isAlive() | Ainda está rodando? |
t.interrupt() | Pede para t parar de esperar (sleep/join lançam InterruptedException) |
Thread.ofPlatform().name("…").start(tarefa) | Cria, nomeia e inicia (Java 21+) |
Exercícios
1. Complete para iniciar a thread t de verdade:
2. Complete para a main esperar a thread t terminar:
3. Complete para pausar a thread atual por meio segundo:
4. Duas threads mostram 5 linhas cada uma. Rodando o programa várias vezes, o que acontece?
5. O que acontece com t.run() no lugar de t.start()?
6. Por que dois caixas fazendo vendas++ 100 000 vezes cada um não chegam a 200 000?
Missão final: este programa chama run(), então a tarefa roda na thread main. Troque para start() e espere a thread com join(), para que apareça Rodando na thread: ajudante.
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable tarefa = () -> System.out.println("Rodando na thread: " + Thread.currentThread().getName());
Thread t = new Thread(tarefa, "ajudante");
t.run();
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable tarefa = () -> System.out.println("Rodando na thread: " + Thread.currentThread().getName());
Thread t = new Thread(tarefa, "ajudante");
t.start();
t.join();
}
}
Resumo
- Uma thread é uma linha de execução independente; todo programa começa na thread
main. new Thread(() -> …)cria;start()inicia (nãorun()!);join()espera;Thread.sleep(ms)pausa.- A ordem entre threads é decidida pelo escalonador: a saída se mistura e muda a cada execução. Dentro de cada thread, a ordem é mantida.
sleepejoinlançamInterruptedException, que é checked.- Threads que alteram a mesma variável perdem dados (condição de corrida): veja synchronized e Locks.
- No editor do guia, as tarefas rodam em sequência e numa ordem fixa; as saídas misturadas desta página são do JDK real.
Próxima aula: ExecutorService, para não criar threads à mão.