Conceitos Fundamentais: Processo vs. Thread
No desenvolvimento de software, é crucial distinguir entre a execução sequencial e a execução paralela ou concorrente. Enquanto métodos comuns seguem um fluxo linear (o método run() executa e o programa aguarda sua conclusão), o multithreading utiliza o método start() para criar múltiplas rotas de execução, permitindo que a thread principal e as threads secundárias processem tarefas alternadamente.
Diferenças entre Processos e Threads
- Processo: É uma instância de um programa em execução. É uma entidade dinâmica que recebe recursos do sistema operacional. Um programa, por si só, é apenas um conjunto estático de instruções.
- Thread: É a unidade básica de execução e agendamento da CPU. Um único processo pode conter várias threads (por exemplo, em um reprodutor de vídeo, existem threads para imagem, som e legendas). Todo processo possui pelo menos uma thread ativa.
- Escalonamento: Em sistemas com apenas um núcleo de CPU, o multithreading real é uma ilusão criada pela alternância rápida entre threads, gerenciada pelo escalonador do sistema operacional.
- Threads de Segundo Plano: Mesmo que você não crie threads mnaualmente, a JVM executa threads automáticas, como a
main()e o Garbage Collector (GC).
Nota importante: O gerenciamento de múltiplas threads introduz custos operacionais, como o tempo de troca de contexto e a necessidade de controle de concorrência. Como cada thread opera em seu próprio espaço de memória de trabalho, a manipulação incorreta de recursos compartilhados pode levar à inconsistência de dados.
Implementação de Threads em Java
Existem duas formas priomrdiais de criar threads em Java: herdando da classe Thread ou implementando a interface Runnable.
- Herdando da Classe Thread
Este método envolve criar uma subclasse que estende java.lang.Thread e sobrescrever o método run().
public class ExecutorDeTarefa extends Thread {
@Override
public void run() {
for (int contador = 0; contador < 50; contador++) {
System.out.println("Thread Secundária - Iteração: " + contador);
}
}
public static void main(String[] args) {
ExecutorDeTarefa minhaThread = new ExecutorDeTarefa();
// Inicia a execução da thread
minhaThread.start();
for (int i = 0; i < 50; i++) {
System.out.println("Fluxo Principal (Main) - Iteração: " + i);
}
}
}
- Implementando a Interface Runnable (Recomendado)
Diferente da herança, a implementação da interface Runnable é preferível porque o Java não suporta herança múltipla. Ao usar interfaces, você mantém a flexibilidade de estender outras classes.
public class TarefaSimples implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("Executando via Runnable: " + i);
}
}
public static void main(String[] args) {
TarefaSimples tarefa = new TarefaSimples();
// Criamos um objeto Thread passando a instância de Runnable
Thread threadExecutora = new Thread(tarefa);
threadExecutora.start();
for (int i = 0; i < 50; i++) {
System.out.println("Executando Main: " + i);
}
}
}
Problemas de Concorrência e Recursos Compartilhados
Quando múltiplas threads tentam acessar e modificar o mesmo recurso simultaneamente, ocorrem problemas de segurança de thread (thread-safety). O exemplo clássico é um sistema de venda de ingressos onde várias pessoas tentam comprar o último assento ao mesmo tempo.
public class SistemaDeVendas implements Runnable {
private int estoqueIngressos = 10;
@Override
public void run() {
while (true) {
if (estoqueIngressos <= 0) {
System.out.println("Ingressos esgotados para: " + Thread.currentThread().getName());
break;
}
// Simulação de latência de rede
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " adquiriu o ingresso n° " + estoqueIngressos--);
}
}
public static void main(String[] args) {
SistemaDeVendas terminal = new SistemaDeVendas();
// Três clientes tentando comprar do mesmo estoque
new Thread(terminal, "Cliente A").start();
new Thread(terminal, "Cliente B").start();
new Thread(terminal, "Cliente C").start();
}
}
No código acima, sem a devida sincronização, é possível que dois clientes recebam o mesmo número de ingressso ou que o estoque fique negativo, demonstrando a instabilidade de dados em ambientes concorrentes.
Simulação de Competição (Exemplo Prático)
Para ilustrar o controle de estado entre threads, considere uma corrida entre dois competidores:
public class Corrida implements Runnable {
private static String vencedor;
@Override
public void run() {
for (int passo = 1; passo <= 100; passo++) {
// Verifica se a corrida já acabou
if (vencedor != null) {
break;
}
// Simula o sono da Lebre a cada 10 passos
if (Thread.currentThread().getName().equals("Lebre") && passo % 10 == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " avançou para o passo " + passo);
validarVitoria(passo);
}
}
private void validarVitoria(int passo) {
if (passo == 100 && vencedor == null) {
vencedor = Thread.currentThread().getName();
System.out.println("O grande vencedor é: " + vencedor);
}
}
public static void main(String[] args) {
Corrida pista = new Corrida();
new Thread(pista, "Lebre").start();
new Thread(pista, "Tartaruga").start();
}
}
Resumo Técnico
- O método
start()é o responsável por solicitar ao sistema operacional a criação de uma nova thread. Chamarrun()diretamente apenas executa o código na thread atual. - A ordem de execução das threads não é garantida; ela depende inteiramente do algoritmo de escalonamento da CPU.
- A interface
Runnableé a abordagem padrão na indústria para desacoplar a tarefa da infraestrutura de threads.