Introdução à concorrência e multithreading em Java
Os aplicativos modernos cada vez mais exigem a execução simultânea de várias tarefas. Seja no processamento de requisições em um servidor, no download de arquivos ou em cálculos complexos — não há como evitar os mecanismos de concorrência e multithreading. Em Java, esses conceitos estão incorporados ao nível da linguagem e da biblioteca padrão, tornando-a uma das melhores ferramentas para criar aplicativos de alto desempenho.
Neste artigo, abordaremos tudo, desde os fundamentos até ferramentas avançadas: como criar threads, sincronizar o acesso a dados e usar utilitários modernos do pacote java.util.concurrent. O material será útil tanto para iniciantes quanto para desenvolvedores que desejam sistematizar seus conhecimentos.
O que é uma Thread e para que serve?
Uma thread é um processo leve que é executado dentro de um aplicativo. Cada programa Java tem pelo menos uma thread (main). O multithreading permite executar várias threads simultaneamente, o que é especialmente importante em processadores multi-core.
Principais vantagens do multithreading:
- Aumento de desempenho através da execução paralela de tarefas.
- Melhoria na capacidade de resposta da interface do usuário.
- Uso eficiente dos recursos do sistema.
1. Criando threads em Java: abordagens clássicas
Em Java, existem duas maneiras básicas de criar uma thread: herdando da classe Thread e implementando a interface Runnable. Vamos analisar ambas.
Herdando da classe Thread
A maneira mais simples é criar uma subclasse de Thread e sobrescrever o método run().
public class MyThread extends Thread { @Override public void run() { System.out.println("Thread " + getName() + " iniciada."); }
public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // Inicia a thread }}Importante: chame start(), não run(). run() executará o código na thread atual, enquanto start() cria uma nova.
Implementando a interface Runnable
Uma abordagem mais flexível é implementar Runnable e passá-lo ao construtor de Thread.
public class Task implements Runnable { @Override public void run() { System.out.println("Tarefa executando na thread: " + Thread.currentThread().getName()); }
public static void main(String[] args) { Task task = new Task(); Thread thread = new Thread(task); thread.start(); }}Este método é preferível, pois não requer herança e permite que a classe estenda outras classes.
2. Sincronização e atomicidade: como evitar condições de corrida
Quando várias threads acessam dados compartilhados simultaneamente, ocorre uma condição de corrida (race condition). Para evitar isso, o Java fornece mecanismos de sincronização.
A palavra-chave synchronized
Com synchronized, você pode bloquear um método ou bloco de código para uma única thread.
public class Counter { private int count = 0;
public synchronized void increment() { count++; }
public int getCount() { return count; }}Neste exemplo, apenas uma thread pode executar increment() em um determinado momento. No entanto, para operações simples, como incremento, é melhor usar classes atômicas.
Classes atômicas (Atomic)
O pacote java.util.concurrent.atomic contém classes que fornecem operações atômicas sem bloqueios.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter { private A