DEV Community

Cover image for Explorando o Multitarefa com Threads em Java
Antonio Serra
Antonio Serra

Posted on Edited on

Explorando o Multitarefa com Threads em Java

Explorando o Multitarefa com Threads em Java

Imagine que você está na cozinha preparando o almoço. Enquanto o arroz cozinha, você pica a cebola. Enquanto a carne frita, você lava a louça. Você não espera uma tarefa terminar para começar a outra — você faz várias coisas ao mesmo tempo.

É exatamente isso que a programação multitarefa (ou multithreading) faz: permite que um programa execute várias tarefas simultaneamente, em vez de uma depois da outra.


Processo x Thread: qual a diferença?

Conceito O que é Analogia
Processo Um programa em execução, com sua própria memória A cozinha inteira
Thread Uma linha de execução dentro do processo Cada cozinheiro trabalhando na cozinha

Vários cozinheiros (threads) dividem a mesma cozinha (memória do processo). Isso é rápido e eficiente — mas, como veremos, também pode gerar confusão se dois cozinheiros pegarem a mesma panela ao mesmo tempo.

Todo programa Java já nasce com pelo menos uma thread: a thread principal (main).


Por que usar threads?

  • Não travar a interface: enquanto o programa baixa um arquivo, o usuário continua clicando nos botões.
  • Aproveitar o processador: computadores modernos têm vários núcleos. Sem threads, você usa só um.
  • Fazer tarefas independentes em paralelo: baixar 3 arquivos ao mesmo tempo é mais rápido que baixar um por vez.

O problema: código sequencial

Vamos começar com um exemplo sem threads:

public class SemThreads {
    public static void main(String[] args) {
        tarefa("Baixando arquivo");
        tarefa("Processando imagem");
        System.out.println("Fim!");
    }

    static void tarefa(String nome) {
        for (int i = 1; i <= 3; i++) {
            System.out.println(nome + " - passo " + i);
            try {
                Thread.sleep(500); // simula um trabalho demorado
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Saída:

Baixando arquivo - passo 1
Baixando arquivo - passo 2
Baixando arquivo - passo 3
Processando imagem - passo 1
Processando imagem - passo 2
Processando imagem - passo 3
Fim!
Enter fullscreen mode Exit fullscreen mode

Note que a segunda tarefa só começa depois que a primeira termina. Tempo total: 3 segundos.


Criando threads em Java

Existem três formas principais. Vamos da mais antiga para a mais moderna.

1. Herdando de Thread

class MinhaThread extends Thread {
    private String nome;

    public MinhaThread(String nome) {
        this.nome = nome;
    }

    @Override
    public void run() {          // o que a thread vai executar
        for (int i = 1; i <= 3; i++) {
            System.out.println(nome + " - passo " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ExemploThread {
    public static void main(String[] args) {
        MinhaThread t1 = new MinhaThread("Baixando arquivo");
        MinhaThread t2 = new MinhaThread("Processando imagem");

        t1.start();   // ATENÇÃO: start(), não run()!
        t2.start();

        System.out.println("Main continua livre!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Saída (aproximada — a ordem pode variar!):

Main continua livre!
Baixando arquivo - passo 1
Processando imagem - passo 1
Baixando arquivo - passo 2
Processando imagem - passo 2
Baixando arquivo - passo 3
Processando imagem - passo 3
Enter fullscreen mode Exit fullscreen mode

Agora as duas tarefas rodam juntas. Tempo total: 1,5 segundo. 🚀

⚠️ Erro clássico de aluno: chamar t1.run() em vez de t1.start().
run() executa o código na thread atual (sequencial, sem paralelismo nenhum).
start() cria uma nova thread de verdade e chama run() dentro dela.

2. Implementando Runnable (recomendado)

Java não permite herança múltipla. Se sua classe já herda de outra, extends Thread fica impossível. Por isso, Runnable é a forma preferida:

class MinhaTarefa implements Runnable {
    private String nome;

    public MinhaTarefa(String nome) {
        this.nome = nome;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(nome + " - passo " + i);
        }
    }
}

public class ExemploRunnable {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MinhaTarefa("Tarefa A"));
        Thread t2 = new Thread(new MinhaTarefa("Tarefa B"));

        t1.start();
        t2.start();
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Com expressão lambda (Java 8+)

Para tarefas curtas, fica muito mais enxuto:

public class ExemploLambda {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                System.out.println("Tarefa A - passo " + i);
            }
        });

        Thread t2 = new Thread(() ->
            System.out.println("Tarefa B executando!")
        );

        t1.start();
        t2.start();
    }
}
Enter fullscreen mode Exit fullscreen mode

Métodos essenciais da classe Thread

Método O que faz
start() Inicia a thread (chama run() em paralelo)
run() Contém o código a ser executado
Thread.sleep(ms) Pausa a thread por X milissegundos
join() Faz a thread atual esperar outra terminar
getName() / setName() Lê ou define o nome da thread
isAlive() Retorna true se a thread ainda está rodando

Exemplo com join()

Sem join(), a main pode terminar antes das outras threads:

public class ExemploJoin {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> System.out.println("Somando..."));
        Thread t2 = new Thread(() -> System.out.println("Calculando média..."));

        t1.start();
        t2.start();

        t1.join();   // espera t1 terminar
        t2.join();   // espera t2 terminar

        System.out.println("Todos os cálculos foram concluídos!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Sem os join(), a mensagem final poderia aparecer antes dos cálculos.


Cuidado! O problema da condição de corrida

Quando duas threads mexem no mesmo dado ao mesmo tempo, o resultado pode ficar errado. Veja:

class Contador {
    int valor = 0;

    void incrementar() {
        valor++;   // NÃO é uma operação única!
    }
}

public class ProblemaCorrida {
    public static void main(String[] args) throws InterruptedException {
        Contador c = new Contador();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) c.incrementar();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) c.incrementar();
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Valor esperado: 20000");
        System.out.println("Valor obtido:   " + c.valor);
    }
}
Enter fullscreen mode Exit fullscreen mode

Resultado típico: algo como 18437 — e muda a cada execução!

Por quê? valor++ são na verdade 3 passos: ler o valor, somar 1, gravar de volta. Se as duas threads lerem 5 ao mesmo tempo, as duas gravam 6. Um incremento se perdeu.

A solução: synchronized

A palavra-chave synchronized garante que só uma thread por vez entre no método:

class Contador {
    private int valor = 0;

    public synchronized void incrementar() {
        valor++;
    }

    public int getValor() {
        return valor;
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora o resultado será sempre 20000. ✅

💡 Pense no synchronized como uma chave do banheiro: só entra quem pegar a chave, e os outros esperam na fila.


Bônus: o jeito moderno com ExecutorService

Criar threads manualmente é caro. Em aplicações reais, usamos um pool de threads que reaproveita threads prontas:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExemploExecutor {
    public static void main(String[] args) throws InterruptedException {
        // Pool com 3 threads reutilizáveis
        ExecutorService pool = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 6; i++) {
            int tarefa = i;
            pool.submit(() -> {
                System.out.println("Tarefa " + tarefa +
                    " rodando em " + Thread.currentThread().getName());
            });
        }

        pool.shutdown();  // não aceita novas tarefas
        pool.awaitTermination(1, TimeUnit.MINUTES);
        System.out.println("Todas as tarefas finalizadas!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui, 6 tarefas são distribuídas entre apenas 3 threads — muito mais eficiente.


Resumo rápido

  • Thread = linha de execução independente dentro do programa.
  • Crie threads com extends Thread, implements Runnable (preferível) ou lambda.
  • Sempre chame start(), nunca run() diretamente.
  • Use join() para esperar uma thread terminar.
  • Dados compartilhados exigem sincronização (synchronized), senão ocorrem condições de corrida.
  • Em projetos reais, prefira ExecutorService em vez de criar threads na mão.

Exercícios propostos

  1. Crie duas threads: uma que imprime os números pares de 1 a 20 e outra que imprime os ímpares. Observe como a ordem varia a cada execução.
  2. Simule uma conta bancária com saldo de R$ 1.000 e 5 threads sacando R$ 100 cada. Rode sem synchronized, veja o problema, e depois corrija.
  3. Use ExecutorService para calcular o fatorial dos números de 1 a 10, cada um em uma tarefa diferente.

Dica final: a maior dificuldade com threads não é criá-las — é coordená-las. Comece simples, sempre teste o que acontece quando duas threads acessam o mesmo dado, e a lógica vai ficando natural.

Bons estudos! 👨‍💻👩‍💻

Repositório com a trilha de java: https://github.com/antoniorws/basic-java

LinkedIn: https://www.linkedin.com/in/antonio-rodrigo-wanderley-serra/
GitHub: https://github.com/antoniorws

Top comments (0)