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();
}
}
}
}
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!
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!");
}
}
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
Agora as duas tarefas rodam juntas. Tempo total: 1,5 segundo. 🚀
⚠️ Erro clássico de aluno: chamar
t1.run()em vez det1.start().
run()executa o código na thread atual (sequencial, sem paralelismo nenhum).
start()cria uma nova thread de verdade e chamarun()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();
}
}
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();
}
}
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!");
}
}
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);
}
}
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;
}
}
Agora o resultado será sempre 20000. ✅
💡 Pense no
synchronizedcomo 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!");
}
}
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(), nuncarun()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
ExecutorServiceem vez de criar threads na mão.
Exercícios propostos
- 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.
- 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. - Use
ExecutorServicepara 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)