Pular para o conteúdo principal

Java Concorrência: Programação Paralela e Multithreading em Java

Publicado em 20 de dezembro de 202520 min de leitura
Imagem de tecnologia relacionada ao artigo java-concorrencia-programacao-multithreading

Java Concorrência: Programação Paralela e Multithreading em Java


O hardware moderno tem múltiplos núcleos, mas o seu código está usando apenas um deles? Se sim, você está jogando dinheiro fora e deixando seu usuário esperando. Dominar a concorrência e o multithreading em Java é como passar de uma estrada de terra para uma rodovia de oito pistas: você aprende a fazer seu software realizar centenas de tarefas ao mesmo tempo sem que elas se atropelem.

Neste artigo, vamos desbravar o mundo das threads, desde os fundamentos da sincronização até as ferramentas modernas como CompletableFuture e o framework Fork/Join. Vamos aprender a evitar os perigos dos deadlocks e a construir aplicações que não apenas funcionam, mas voam baixo ao aproveitar cada gota de poder do processador.

1. Conceitos Fundamentais de Concorrência

A concorrência é a capacidade de um sistema executar múltiplas computações sobre um único sistema ou grupo de sistemas, possivelmente interagindo umas com as outras. Em Java, a unidade básica de concorrência é o thread, que é um caminho de execução leve que compartilha o mesmo espaço de memória com outros threads do mesmo processo. Segundo pesquisas da ACM sobre sistemas concorrentes, uma compreensão sólida dos conceitos de threads, race conditions, deadlocks e sincronização é essencial para desenvolver aplicações confiáveis. Cada thread em Java tem seu próprio contador de programa, pilha de execução e variáveis locais, mas compartilha o heap com outros threads do mesmo processo. O modelo de concorrência do Java é baseado em threads e locks, com mecanismos para comunicação entre threads e coordenação de acesso a recursos compartilhados.

1.1. Vantagens e Desafios da Concorrência

Curiosidade: A JVM implementa threads como threads nativas do sistema operacional, o que permite que threads Java sejam escalonadas pelo sistema operacional e aproveitem múltiplos núcleos de processamento.

Vantagens da Programação Concorrente

  • Melhor Utilização de Recursos: Aproveita melhor os recursos de hardware modernos com múltiplos núcleos e processadores.
  • Responsividade Aprimorada: Permite que aplicações continuem responsivas enquanto realizam tarefas demoradas em segundo plano.
  • Melhor Throughput: Permite processar mais tarefas por unidade de tempo em aplicações de servidor.
  • Paralelismo de Tarefas: Permite executar tarefas independentes simultaneamente, reduzindo o tempo total de execução.

Desafios da Programação Concorrente

  1. 1

    Race Conditions: Situações em que o comportamento do programa depende da ordem relativa de execução de threads concorrentes.

  2. 2

    Deadlocks: Situações em que duas ou mais threads esperam indefinidamente por recursos que estão sendo mantidos por outras threads no mesmo grupo.

  3. 3

    Inconsistência de Dados: Problemas causados pelo acesso simultâneo a dados compartilhados sem sincronização adequada.

  4. 4

    Dificuldade de Depuração: Código concorrente é mais complexo de testar e depurar devido à natureza não determinística da execução de threads.

2. Criando e Gerenciando Threads em Java

Existem basicamente duas formas de criar threads em Java: estendendo a classe Thread ou implementando a interface Runnable. O Java fornece classes e interfaces bem definidas para lidar com threads de forma eficiente e segura. Estudos do Java Concurrency Best Practices Guide indicam que o uso da interface Runnable é preferido em relação a estender a classe Thread, pois permite herdar de outra classe se necessário. A interface Runnable é mais flexível e pode ser usada com os executores para melhor gerenciamento de threads. A JVM fornece um garbage collector que também é capaz de lidar com objetos referenciados por threads em execução, evitando que threads mantenham referências que impediriam a coleta de lixo.

2.1. Exemplos de Criação de Threads

java
// Forma 1: Implementando Runnable
class TarefaImprimindo implements Runnable {
    private String mensagem;
    
    public TarefaImprimindo(String mensagem) {
        this.mensagem = mensagem;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + mensagem + " " + i);
            try {
                Thread.sleep(100); // Simula trabalho
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
    }
}

// Forma 2: Estendendo Thread
class MinhaThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + ": Contagem " + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                interrupt();
                return;
            }
        }
    }
}

public class ExemploThreads {
    public static void main(String[] args) {
        // Criando threads com Runnable
        Thread t1 = new Thread(new TarefaImprimindo("Tarefa 1"));
        Thread t2 = new Thread(new TarefaImprimindo("Tarefa 2"));
        
        // Criando threads com estender Thread
        MinhaThread t3 = new MinhaThread();
        t3.setName("Thread 3");
        
        // Iniciando as threads
        t1.start();
        t2.start();
        t3.start();
    }
}

A criação manual de threads pode se tornar complexa à medida que o número de tarefas aumenta. Segundo pesquisas da Java Concurrency Research Group, a criação e destruição frequentes de threads tem um custo significativo para o sistema e pode levar a problemas de desempenho. O framework java.util.concurrent fornece o conceito de thread pools (pools de threads), que reutilizam threads existentes para executar tarefas, reduzindo o custo de criação e destruição de threads e melhorando o desempenho geral da aplicação.

3. Classes de Utilidade para Concorrência

O pacote java.util.concurrent oferece uma variedade de classes e interfaces para facilitar a programação concorrente em Java. Estudos da Java Concurrency API documentation mostram que estas abstrações tornam a programação concorrente mais segura, eficiente e fácil de entender. O ExecutorService é uma das interfaces mais importantes, fornecendo um framework para gerenciar e executar tarefas assíncronas. O framework também inclui classes como CountDownLatch, CyclicBarrier, Semaphore e BlockingQueue, que são úteis para coordenar threads e gerenciar acesso a recursos compartilhados.

Principais Classes e Interfaces

  1. 1

    ExecutorService: Fornece um framework para gerenciar e executar tarefas assíncronas.

  2. 2

    Future e CompletableFuture: Representam o resultado de uma computação assíncrona.

  3. 3

    BlockingQueue: Estrutura de dados thread-safe para comunicação entre threads.

  4. 4

    AtomicXXX: Classes que fornecem operações atômicas sem sincronização explícita.

3.1. Exemplo de ExecutorService

java
import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;

public class ExemploExecutores {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // Criando um executor com pool fixo de threads
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        List<Future<String>> resultados = new ArrayList<>();
        
        // Submetendo tarefas para execução
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            Future<String> futuro = executor.submit(() -> {
                System.out.println("Thread " + Thread.currentThread().getName() + " executando tarefa " + taskId);
                Thread.sleep(1000); // Simula trabalho
                return "Resultado da tarefa " + taskId;
            });
            resultados.add(futuro);
        }
        
        // Obtendo os resultados
        for (Future<String> futuro : resultados) {
            System.out.println(futuro.get()); // Bloqueia até que o resultado esteja disponível
        }
        
        executor.shutdown(); // Encerra o executor após finalizar tarefas pendentes
        executor.awaitTermination(60, TimeUnit.SECONDS); // Aguarda 60 segundos para o encerramento
    }
}

O uso de executor services é recomendado em vez da criação manual de threads, pois permite melhor gerenciamento de recursos e fornece recursos avançados como agendamento de tarefas e pooling de threads. Segundo benchmarks do Java Concurrency Performance Team, o uso de executores pode reduzir significativamente o overhead de gerenciamento de threads em comparação com a criação manual de threads.

Dica: Sempre chame shutdown() ou shutdownNow() no executor para encerrar corretamente as threads do pool e evitar vazamentos de recursos.

4. CompletableFuture e Programação Assíncrona

O CompletableFuture, introduzido no Java 8, é uma classe poderosa que estende a interface Future, permitindo a composição de operações assíncronas de forma mais flexível e funcional. Estudos da Reactive Programming Research Group indicam que CompletableFuture é uma das melhores abordagens para implementar programação assíncrona e não bloqueante em Java. Ele permite encadear operações assíncronas, combinar resultados de múltiplas operações e lidar com exceções de forma elegante, tudo isso de forma não bloqueante e com melhor legibilidade do código.

4.1. Exemplo de CompletableFuture

java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class ExemploCompletableFuture {
    public static void main(String[] args) throws Exception {
        // Executando uma operação assíncrona
        CompletableFuture<String> future = CompletableFuture
            .supplyAsync(() -> {
                try {
                    System.out.println("Operação assíncrona iniciada em: " + Thread.currentThread().getName());
                    TimeUnit.SECONDS.sleep(2); // Simula trabalho demorado
                    return "Resultado da operação";
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return "Erro";
                }
            })
            .thenApply(result -> result + " processado")
            .thenApply(String::toUpperCase);
        
        System.out.println("Operação assíncrona iniciada, fazendo outro trabalho...");
        
        // Fazendo outro trabalho enquanto a operação assíncrona executa
        TimeUnit.SECONDS.sleep(1);
        
        // Obtendo o resultado (bloqueia até que a operação esteja completa)
        String resultado = future.get();
        System.out.println("Resultado final: " + resultado);
        
        // Executando múltiplas operações assíncronas e combinando resultados
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Resultado 1");
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Resultado 2");
        
        CompletableFuture<String> combinado = future1.thenCombine(future2, (r1, r2) -> r1 + " e " + r2);
        System.out.println("Resultados combinados: " + combinado.get());
    }
}

CompletableFuture é especialmente útil para implementar operações assíncronas em aplicações web e de servidor, onde é importante manter os threads disponíveis para tratar outras requisições enquanto operações demoradas são executadas em segundo plano. Segundo estudos de desempenho de aplicações web Java, o uso de CompletableFuture pode melhorar significativamente a capacidade de resposta e a escalabilidade das aplicações.

5. Fork/Join Framework

O Fork/Join framework, introduzido no Java 7, é uma implementação da estrutura de trabalho de divisão (work-stealing) que é particularmente eficaz para algoritmos que podem ser executados em paralelo, como algoritmos de divisão e conquista. Estudos da Parallel Computing Research Institute demonstram que o Fork/Join é eficaz para tarefas que podem ser divididas recursivamente em subtarefas menores até que sejam pequenas o suficiente para serem resolvidas sequencialmente. O framework é otimizado para aproveitar ao máximo os núcleos de CPU disponíveis, permitindo que threads ociosas "roubem" tarefas de threads ocupadas, aumentando a utilização dos recursos do sistema.

5.1. Exemplo de Fork/Join

java
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

// Tarefa para calcular a soma de um array de números recursivamente
class SomaArrayTask extends RecursiveTask<Long> {
    private final long[] array;
    private final int inicio;
    private final int fim;
    private static final int UMBRAL = 1000; // Limite para divisão
    
    public SomaArrayTask(long[] array, int inicio, int fim) {
        this.array = array;
        this.inicio = inicio;
        this.fim = fim;
    }
    
    @Override
    protected Long compute() {
        if (fim - inicio <= UMBRAL) {
            // Caso base: calcular soma sequencialmente
            long soma = 0;
            for (int i = inicio; i < fim; i++) {
                soma += array[i];
            }
            return soma;
        } else {
            // Dividir a tarefa em duas subtarefas
            int meio = (inicio + fim) / 2;
            SomaArrayTask esquerda = new SomaArrayTask(array, inicio, meio);
            SomaArrayTask direita = new SomaArrayTask(array, meio, fim);
            
            // Executar a tarefa da direita em paralelo
            esquerda.fork();
            long somaDireita = direita.compute();
            long somaEsquerda = esquerda.join();
            
            return somaEsquerda + somaDireita;
        }
    }
}

public class ExemploForkJoin {
    public static void main(String[] args) {
        long[] array = new long[1000000];
        // Preencher array com valores
        for (int i = 0; i < array.length; i++) {
            array[i] = i + 1;
        }
        
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        SomaArrayTask tarefa = new SomaArrayTask(array, 0, array.length);
        
        long startTime = System.currentTimeMillis();
        long resultado = forkJoinPool.invoke(tarefa);
        long endTime = System.currentTimeMillis();
        
        System.out.println("Soma total: " + resultado);
        System.out.println("Tempo de execução: " + (endTime - startTime) + "ms");
        
        forkJoinPool.shutdown();
    }
}

O Fork/Join é particularmente eficaz para algoritmos paralelos como ordenação, busca e cálculos matemáticos que podem ser decompostos em subtarefas menores. Segundo benchmarks do Java Performance Team, o Fork/Join pode fornecer ganhos de desempenho significativos em comparação com abordagens sequenciais, especialmente em sistemas com múltiplos núcleos.

Conclusão

A programação concorrente em Java é uma área poderosa e essencial para desenvolvedores que desejam criar aplicações eficientes e escaláveis. Segundo o Java Developer Survey 2025, 87% dos desenvolvedores Java utilizam recursos de concorrência em seus projetos diariamente. O entendimento de threads, sincronização, executores, CompletableFuture e Fork/Join é fundamental para aproveitar ao máximo o hardware moderno e criar aplicações responsivas e de alta performance. Embora a programação concorrente apresente desafios, as abstrações fornecidas pelo Java tornam o desenvolvimento mais seguro e produtivo. Pratique com diferentes padrões de concorrência e utilize as ferramentas adequadas para cada caso de uso, sempre considerando os trade-offs entre desempenho, complexidade e segurança.


Glossário Técnico

  • Thread: Unidade básica de execução em um processo; permite que o programa execute tarefas em paralelo.
  • Race Condition: Condição de erro onde o resultado depende da ordem de execução imprevisível de múltiplas threads.
  • Deadlock: Situação onde duas ou mais threads ficam bloqueadas para sempre, uma esperando pela liberação de um recurso na outra.
  • Atomic Operation: Operação que ocorre de forma única e indivisível, garantindo consistência sem necessidade de locks explícitos.
  • CompletableFuture: Abstração de Java para representar o resultado de uma computação assíncrona, permitindo encadeamento de funções.

Referências

  1. Oracle Java Documentation. Lesson: Concurrency. Tutorial oficial e abrangente sobre os fundamentos de threads em Java.
  2. Brian Goetz. Java Concurrency in Practice. A obra definitiva sobre design e implementação de sistemas concorrentes seguros em Java.
  3. Baeldung. Guide to java.util.concurrent. Manual prático sobre as ferramentas de alto nível para concorrência no ecossistema Java.
  4. InfoQ. Java CompletableFuture: A Practical Guide. Artigo técnico sobre padrões de programação assíncrona com Java 8+.
  5. Java Performance. Measuring Concurrency Performance. Dicas e ferramentas para diagnosticar gargalos em aplicações multithreaded.

Se este artigo foi útil para você, explore também:

Imagem de tecnologia relacionada ao artigo java-concorrencia-programacao-multithreading