- Exemplo Básico com Herança da Clasce Thread
O primeiro paradigma para criar threads em Java envolve a extensão da classe Thread. A implementação requer a sobrescrita do método run(), que contém a lógica a ser executada de forma concorrente.
public class ProcessadorTarefa extends Thread {
private String identificador;
public ProcessadorTarefa(String id) {
this.identificador = id;
}
@Override
public void run() {
for (int contador = 0; contador < 5; contador++) {
System.out.println(identificador + " executando... " + contador);
}
}
public static void main(String[] args) {
ProcessadorTarefa tarefa1 = new ProcessadorTarefa("Processo-A");
ProcessadorTarefa tarefa2 = new ProcessadorTarefa("Processo-B");
ProcessadorTarefa tarefa3 = new ProcessadorTarefa("Processo-C");
tarefa1.start();
tarefa2.start();
tarefa3.start();
}
}
Resultado de uma execução:
Processo-B executando... 0
Processo-B executando... 1
Processo-C executando... 0
Processo-A executando... 0
Processo-B executando... 2
Processo-C executando... 1
Processo-A executando... 1
Processo-B executando... 3
Processo-C executando... 2
Processo-A executando... 2
Processo-B executando... 4
Processo-C executando... 3
Processo-A executando... 3
Processo-C executando... 4
Processo-A executando... 4
- Exemplo com Implementação da Interface Runnable
A abordagem preferencial utiliza a interface Runnable, que oferece maior flexibilidade arquitetural. A classe Thread atua como um invólucro (wrapper) para a instância Runnable.
public class ExecutorTrabalho implements Runnable {
private String nomeThread;
public ExecutorTrabalho(String nome) {
this.nomeThread = nome;
}
@Override
public void run() {
for (int iteracao = 0; iteracao < 5; iteracao++) {
System.out.println(nomeThread + " processando... " + iteracao);
}
}
public static void main(String[] args) {
ExecutorTrabalho trabalhoA = new ExecutorTrabalho("Worker-A");
ExecutorTrabalho trabalhoB = new ExecutorTrabalho("Worker-B");
ExecutorTrabalho trabalhoC = new ExecutorTrabalho("Worker-C");
new Thread(trabalhoA).start();
new Thread(trabalhoB).start();
new Thread(trabalhoC).start();
}
}
Resultado observado:
Worker-A processando... 0
Worker-A processando... 1
Worker-A processando... 2
Worker-B processando... 0
Worker-C processando... 0
Worker-A processando... 3
Worker-B processando... 1
Worker-C processando... 1
Worker-B processando... 2
Worker-A processando... 4
Worker-C processando... 2
Worker-B processando... 3
Worker-C processando... 3
Worker-B processando... 4
Worker-C processando... 4
- Compartilhamento de Recursos: Venda de Ingressos
A vantagem crucial de Runnable emerge quando múltiplas threads devem compartilhar um mesmo recurso. Considere um sistema de vendas onde vários caixas operam em um único estoque de ingressos.
public class GerenciadorIngressos implements Runnable {
private int ingressosRestantes = 5;
@Override
public void run() {
for (int tentativa = 0; tentativa < 20; tentativa++) {
venderIngresso();
}
}
private void venderIngresso() {
if (ingressosRestantes > 0) {
System.out.println(Thread.currentThread().getName() + " vendeu ingresso nº " + ingressosRestantes);
ingressosRestantes--;
}
}
public static void main(String[] args) {
GerenciadorIngressos gerenciador = new GerenciadorIngressos();
new Thread(gerenciador, "Caixa-Norte").start();
new Thread(gerenciador, "Caixa-Sul").start();
new Thread(gerenciador, "Caixa-Leste").start();
}
}
Execução típica:
Caixa-Norte vendeu ingresso nº 5
Caixa-Sul vendeu ingresso nº 4
Caixa-Leste vendeu ingresso nº 3
Caixa-Norte vendeu ingresso nº 2
Caixa-Sul vendeu ingresso nº 1
- Vantagens da Abordagem Runnable
A implementação de Runnable oferece benefícios significativos:
- Compartilhamento eficiente: Várias threads podem executar a mesma instância Runnable, operando sobre recursos comuns naturalmente.
- Flexibilidade de herança: Permite que a classe principal estenda outras superclasses, contornando a limitação de herança única do Java.
- Separação de responsabilidades: Isola a lógica de execução (
run()) do mecanismo de threading (Thread). - Reutilização de código: A mesma tarefa pode ser executada em diferentes contextos thread.
- Identificação e Nomenclatura de Threads
public class DemonstracaoNomes implements Runnable {
@Override
public void run() {
for (int ciclo = 0; ciclo < 3; ciclo++) {
System.out.println("Thread ativa: " + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
DemonstracaoNomes tarefa = new DemonstracaoNomes();
new Thread(tarefa, "Processo-Alfa").start();
new Thread(tarefa, "Processo-Beta").start();
new Thread(tarefa).start(); // Nome padrão: Thread-0, Thread-1, etc.
}
}
A JVM atribui nomes automáticos quando não especificados explicitamente. O método main executa em sua própria thread, e a coleta de lixo também opera em thread dedicada. Cada execução de programa Java inicia no mínimo duas threads: a principal e a de garbage collection.
- Verificando Estado de Execução
public class MonitoramentoEstado implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("Executando: " + Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
MonitoramentoEstado tarefa = new MonitoramentoEstado();
Thread threadMonitorada = new Thread(tarefa);
System.out.println("Antes do start(): " + threadMonitorada.isAlive());
threadMonitorada.start();
System.out.println("Após start(): " + threadMonitorada.isAlive());
Thread.sleep(100); // Aguarda término
System.out.println("Após execução: " + threadMonitorada.isAlive());
}
}
- Aguardando Conclusão com join()
O método join() força a thread atual a aguardar a finalização de outra thread antes de prosseguir.
public class EsperaThread implements Runnable {
@Override
public void run() {
for (int iteracao = 0; iteracao < 10; iteracao++) {
System.out.println(Thread.currentThread().getName() + " - passo " + iteracao);
}
}
public static void main(String[] args) {
Thread threadSecundaria = new Thread(new EsperaThread(), "ThreadSecundaria");
threadSecundaria.start();
for (int i = 0; i < 10; i++) {
if (i == 5) {
try {
threadSecundaria.join(); // Main aguarda threadSecundaria
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("MainThread - passo " + i);
}
}
}
- Pausa Temporária com sleep()
public class PausaExecucao implements Runnable {
@Override
public void run() {
for (int repeticao = 0; repeticao < 3; repeticao++) {
System.out.println(new java.util.Date());
try {
Thread.sleep(2000); // Pausa de 2 segundos
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " retomou após pausa");
}
}
public static void main(String[] args) {
new Thread(new PausaExecucao(), "ThreadPausavel").start();
}
}
- Interrupção de Thread
public class InterrupcaoDemo implements Runnable {
@Override
public void run() {
System.out.println("Início da execução: " + java.time.LocalTime.now());
try {
Thread.sleep(10000);
System.out.println("Sleep concluído normalmente");
} catch (InterruptedException e) {
System.out.println("Thread interrompida durante sleep!");
return; // Encerra execução
}
System.out.println("Fim da execução: " + java.time.LocalTime.now());
}
public static void main(String[] args) throws InterruptedException {
Thread threadInterrompivel = new Thread(new InterrupcaoDemo());
threadInterrompivel.start();
Thread.sleep(2000);
threadInterrompivel.interrupt(); // Solicita interrupção
System.out.println("Interrupção solicitada: " + java.time.LocalTime.now());
}
}
- Threads Daemon
Threads daemon servem como serviçais para threads de usuário. A JVM finaliaz quando apenas threads daemon permanecem ativas.
public class ServicoDaemon implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("Serviço em segundo plano: " + Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
break;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread daemon = new Thread(new ServicoDaemon(), "DaemonThread");
daemon.setDaemon(true); // Marca como daemon
daemon.start();
System.out.println("Thread principal finalizando...");
Thread.sleep(2000); // Após isso, a daemon morre
}
}
- Prioridades de Thread
public class ControlePrioridade implements Runnable {
@Override
public void run() {
for (int passo = 0; passo < 5; passo++) {
System.out.println(Thread.currentThread().getName() + " prioridade " +
Thread.currentThread().getPriority() + " - " + passo);
}
}
public static void main(String[] args) {
Thread altaPrioridade = new Thread(new ControlePrioridade(), "AltaPrioridade");
Thread baixaPrioridade = new Thread(new ControlePrioridade(), "BaixaPrioridade");
Thread mediaPrioridade = new Thread(new ControlePrioridade(), "MediaPrioridade");
altaPrioridade.setPriority(Thread.MAX_PRIORITY); // 10
baixaPrioridade.setPriority(Thread.MIN_PRIORITY); // 1
mediaPrioridade.setPriority(Thread.NORM_PRIORITY); // 5
baixaPrioridade.start();
mediaPrioridade.start();
altaPrioridade.start();
}
}
- Cedendo Execução com yield()
O método yield() sugere ao scheduler que a thread atual pode ceder seu timeslice.
public class CedenciaThread implements Runnable {
@Override
public void run() {
for (int rodada = 0; rodada < 5; rodada++) {
System.out.println(Thread.currentThread().getName() + " rodada " + rodada);
if (rodada == 2) {
System.out.println(Thread.currentThread().getName() + " cedendo vez...");
Thread.yield();
}
}
}
public static void main(String[] args) {
new Thread(new CedenciaThread(), "Competidor-1").start();
new Thread(new CedenciaThread(), "Competidor-2").start();
}
}
- Sincronização de Acesso
Para evitar condições de corrida, utilize o bloco synchronized:
public class ContadorSeguro implements Runnable {
private int contador = 5;
@Override
public void run() {
for (int tentativa = 0; tentativa < 10; tentativa++) {
synchronized (this) {
if (contador > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " valor: " + contador--);
}
}
}
}
public static void main(String[] args) {
ContadorSeguro contador = new ContadorSeguro();
new Thread(contador, "Acesso-1").start();
new Thread(contador, "Acesso-2").start();
new Thread(contador, "Acesso-3").start();
}
}
- Métodos Sincronizados
Alternativa ao bloco synchronized: métodos sincronizados.
public class OperacaoAtomica implements Runnable {
private int saldo = 5;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
debitar();
}
}
public synchronized void debitar() {
if (saldo > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " saldo restante: " + saldo--);
}
}
public static void main(String[] args) {
OperacaoAtomica operacao = new OperacaoAtomica();
new Thread(operacao, "Transacao-1").start();
new Thread(operacao, "Transacao-2").start();
new Thread(operacao, "Transacao-3").start();
}
}
A sincronização é essencial para consistência de dados, mas o uso excessivo pode causar deadlocks, especialmente em cenários complexos como o problema clássico de produtor-consumidor.