Article image
Wallace Sousa
Wallace Sousa21/10/2025 19:04
Compartilhe

💻 Threads, Concorrência e Sincronização em Java: Da Teoria à Prática

    A programação Java é conhecida por sua robustez e capacidade de lidar com sistemas complexos. Um dos pilares que tornam isso possível é o suporte nativo à concorrência — a habilidade de executar várias tarefas simultaneamente.

    Mas, afinal...

    O que são threads? Como o Java gerencia a execução paralela? E por que tantos desenvolvedores enfrentam deadlocks e race conditions sem perceber?

    Neste artigo, vamos entender como o Java trabalha com threads, concorrência e sincronização, desde os fundamentos até recursos modernos como os Virtual Threads, introduzidos no Java 21.

    🧠 O que é Concorrência?

    Imagine que você tem um programa responsável por baixar arquivos da internet, processar imagens e salvar resultados no banco de dados.

    Se ele fizer tudo sequencialmente, o tempo total será enorme.

    A concorrência permite que várias tarefas sejam executadas de forma sobreposta, aproveitando melhor o processador e melhorando o desempenho geral do sistema.

    No Java, isso é feito através das threads.

    ⚙️ Entendendo as Threads

    Uma thread é uma unidade leve de execução dentro de um processo.

    Quando você executa um programa Java, ele inicia automaticamente com uma thread principal chamada main thread.

    Você pode criar novas threads de duas formas principais:

    🧩 1. Estendendo a classe Thread

    
    class MinhaThread extends Thread {
      @Override
      public void run() {
          System.out.println("Executando na thread: " + Thread.currentThread().getName());
      }
    }
    
    public class Exemplo1 {
      public static void main(String[] args) {
          MinhaThread t1 = new MinhaThread();
          t1.start(); // inicia a execução em paralelo
          System.out.println("Thread principal: " + Thread.currentThread().getName());
      }
    }
    

    🧩 2. Implementando a interface Runnable

    
    class Tarefa implements Runnable {
      @Override
      public void run() {
          System.out.println("Executando tarefa em: " + Thread.currentThread().getName());
      }
    }
    
    public class Exemplo2 {
      public static void main(String[] args) {
          Thread t1 = new Thread(new Tarefa());
          t1.start();
      }
    }
    

    Dica:

    O método run() contém o código que será executado pela thread.

    Mas para realmente iniciar uma thread, é preciso chamar start(), e não run() diretamente!

    🔄 Sincronização: Evitando o Caos

    Quando várias threads acessam os mesmos recursos (como variáveis, listas ou arquivos), podem ocorrer condições de corrida (race conditions) — ou seja, o resultado final depende da ordem de execução das threads.

    Exemplo clássico:

    
    public class Contador {
      private int valor = 0;
    
      public void incrementar() {
          valor++;
      }
    
      public int getValor() {
          return valor;
      }
    
      public static void main(String[] args) throws InterruptedException {
          Contador contador = new Contador();
    
          Thread t1 = new Thread(() -> {
              for (int i = 0; i < 1000; i++) contador.incrementar();
          });
          Thread t2 = new Thread(() -> {
              for (int i = 0; i < 1000; i++) contador.incrementar();
          });
    
          t1.start();
          t2.start();
    
          t1.join();
          t2.join();
    
          System.out.println("Valor final: " + contador.getValor());
      }
    }
    

    ⚠️ Problema: O valor final nem sempre será 2000.

    Isso acontece porque duas threads podem acessar valor ao mesmo tempo, causando inconsistência.

    🧱 Solução: o synchronized

    O Java oferece a palavra-chave synchronized para proteger seções críticas do código, garantindo que apenas uma thread por vez execute aquele bloco.

    
    public synchronized void incrementar() {
      valor++;
    }
    

    Agora, apenas uma thread por vez pode modificar o valor — evitando resultados incorretos.

    🔐 Bloqueios e Monitores

    Internamente, o synchronized usa um monitor lock.

    Cada objeto em Java possui um bloqueio implícito, e quando uma thread entra num bloco synchronized, ela “tranca” aquele objeto até terminar.

    Exemplo com bloco sincronizado:

    
    synchronized (this) {
      valor++;
    }
    

    ➡️ É o mesmo efeito que o synchronized no método, mas aplicado apenas dentro de um trecho específico do código.

    ⚡ ExecutorService: Gerenciando Múltiplas Threads

    Criar threads manualmente não é a melhor prática em sistemas grandes.

    O Java fornece o Executor Framework, que gerencia pools de threads automaticamente:

    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ExemploExecutor {
      public static void main(String[] args) {
          ExecutorService executor = Executors.newFixedThreadPool(3);
    
          for (int i = 1; i <= 5; i++) {
              int tarefa = i;
              executor.submit(() -> {
                  System.out.println("Executando tarefa " + tarefa +
                                     " na thread " + Thread.currentThread().getName());
              });
          }
    
          executor.shutdown();
      }
    }
    

    🎯 Aqui, o ExecutorService cria um pool com 3 threads que executam 5 tarefas.

    Enquanto há tarefas sobrando, ele reaproveita as threads, evitando sobrecarga.

    🚀 Java Moderno: Virtual Threads (Project Loom)

    Com o Java 21, foi introduzido um novo tipo de thread: as Virtual Threads — uma revolução na forma de lidar com concorrência.

    Elas são muito mais leves, podendo criar milhares sem sobrecarregar o sistema.

    
    Thread.startVirtualThread(() -> {
      System.out.println("Rodando em uma virtual thread!");
    });
    

    🔍 Enquanto uma thread tradicional é mapeada diretamente para o sistema operacional,

    as virtual threads são gerenciadas pela JVM, permitindo escalabilidade massiva com menor custo de memória.

    💬 Pense nelas como “corrotinas nativas” do Java.

    ⚙️ Classes Úteis de Concorrência

    O pacote java.util.concurrent traz diversas classes para facilitar a vida do desenvolvedor:

    ClasseFunçãoReentrantLockControle avançado de bloqueiosCountDownLatchSincronização de threads até uma condição ser atingidaSemaphoreControle de acesso a recursos limitadosAtomicIntegerOperações atômicas sem synchronizedCompletableFutureExecução assíncrona com retorno de resultado

    🧩 Boas Práticas

    ✔️ Prefira ExecutorService ao invés de criar threads manualmente

    ✔️ Evite sincronizar tudo — isso gera gargalos

    ✔️ Use classes atômicas sempre que possível

    ✔️ Teste e monitore concorrência com cuidado (bugs são sutis e difíceis de reproduzir)

    🎯 Conclusão

    A concorrência é uma das partes mais poderosas (e traiçoeiras) do Java.

    Dominar threads, sincronização e o novo modelo de Virtual Threads é o que separa o desenvolvedor intermediário do engenheiro de software de alto nível.

    O segredo está em entender como a JVM gerencia a execução e usar os recursos certos para cada contexto — sem reinventar a roda.

    ✍️ Autor

    Wallace Candido Maia Sousa

    https://github.com/wallacemaia2007

    https://www.linkedin.com/in/wallacemaia-dev/

    Compartilhe
    Comentários (0)