Fundamentos de Multithreading em Java: Processos, Threads e Concorrência

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.

  1. 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);
        }
    }
}
  1. 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. Chamar run() 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.

Tags: java Multithreading Concurrency JVM threads

Publicado em 7-14 18:05