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.

Você vai aprender
  • 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 com Thread.sleep, dar nomes às threads.
  • As formas de escrever a tarefa (Runnable, lambda, extends Thread), o erro de chamar run() no lugar de start() 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; a main segue para a próxima linha sem esperar.
ana.join()
"Juntar-se": a main para e espera a thread ana terminar.
throws InterruptedException
O join pode 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 dois join.
No editor do guia, a ordem é sempre a mesma

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

Este código tem um erro de propósito.
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
FormaQuando 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 ThreadForma 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.
ExecutorServiceMuitas 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()

Este código tem um erro de lógica de propósito.
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

Este código tem um erro de propósito.
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ódigoO 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:

t.;

2. Complete para a main esperar a thread t terminar:

t.;

3. Complete para pausar a thread atual por meio segundo:

Thread.;

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ão run()!); 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.
  • sleep e join lançam InterruptedException, 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.