VapVup👤
← Voltar
🧵

Concorrência em Java

Concorrência e paralelismo são cruciais para aplicações modernas de alta performance. Em Java, a concorrência evoluiu de manipulação direta de threads no Java 1.0 para abstrações modernas de Executors, CompletableFuture e ForkJoinPool. Compreender esses mecanismos no nível de JVM e CPU é o que separa um desenvolvedor sênior de um que apenas escreve código concorrente instável.

🍳Analogia

A Cozinha de um Restaurante Concorrente

Imagine a CPU do seu servidor como a cozinha de um restaurante renomado. Uma thread é um cozinheiro individual. Runnable é a receita impressa (um conjunto de instruções). Callable é uma receita especial que retorna um prato pronto ao final (valor de retorno). Sem coordenação (sincronização), múltiplos cozinheiros podem tentar cortar legumes na mesma tábua ao mesmo tempo (race conditions), corrompendo os alimentos. Pior ainda, um cozinheiro pode reter a única frigideira esperando que outro libere a única faca, enquanto o outro faz exatamente o oposto (deadlock).

💡Conceito

Threads e Ciclo de Vida na JVM

No Java clássico, cada instância de Thread do Java mapeia diretamente para uma thread de sistema operacional (1:1 platform threads). Chamar thread.start() solicita ao SO o agendamento de uma nova thread, enquanto thread.run() apenas executa o código sequencialmente na thread atual. Uma thread Java passa por seis estados definidos no enum Thread.State: • NEW: Instanciada, mas start() não chamado. • RUNNABLE: Ativa na JVM (executando ou aguardando CPU do SO). • BLOCKED: Aguardando a liberação de um lock de monitor (synchronized). • WAITING: Aguardando notificação ou sinal de outra thread por tempo indeterminado (join(), wait()). • TIMED_WAITING: Aguardando por um período específico (sleep(ms), wait(timeout)). • TERMINATED: Código run() concluído.

// Criação e execução de uma Thread clássica
Thread thread = new Thread(() -> {
    System.out.println("Executando na thread: " + Thread.currentThread().getName());
});
thread.setDaemon(true); // Se for daemon, não impedirá o encerramento da JVM
thread.start(); // Inicia de verdade concorrentemente
🔧Exemplo

Tratando Retornos e Interrupções com Callable

Diferente de Runnable, Callable<V> permite retornar valores e lançar Checked Exceptions. Para interromper threads de maneira segura e cooperativa, usamos o sinal de interrupção (interrupt()). Ao capturar uma InterruptedException, o status de interrupção é limpo, exigindo que o desenvolvedor o restaure explicitamente para notificar a pilha superior.

Callable<String> task = () -> {
    while (!Thread.currentThread().isInterrupted()) {
        try {
            // Operação que pode bloquear
            Thread.sleep(1000);
            return "Sucesso";
        } catch (InterruptedException e) {
            // Restaura flag de interrupção após captura
            Thread.currentThread().interrupt();
            throw e;
        }
    }
    throw new InterruptedException();
};

FutureTask<String> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();
// t.interrupt(); // Cancela cooperativamente
📦Analogia

A Cooperativa de Entregadores (Thread Pools)

Criar uma nova thread no SO é uma operação cara: exige alocação de cerca de 1MB de stack por thread e chamadas de sistema. Contratar e demitir um entregador para cada pacote enviado é ineficiente. Em vez disso, você usa uma cooperativa de entregadores fixos (Thread Pool). Os pacotes chegam em um galpão central (BlockingQueue) e os entregadores disponíveis pegam o pacote da vez. Quando não há entregas, eles simplesmente aguardam no galpão, reduzindo drasticamente o overhead.

💡Conceito

ExecutorService e CompletableFuture

A API ExecutorService gerencia pools de threads de forma robusta. Contudo, construtores de conveniência em Executors (como newFixedThreadPool que usa fila ilimitada, ou newCachedThreadPool que cria threads infinitamente) podem causar OutOfMemoryError em produção sob estresse. Prefira instanciar ThreadPoolExecutor diretamente com limites de fila e políticas de rejeição definidas. Para fluxos de trabalho assíncronos não-bloqueantes mais complexos, o CompletableFuture (Java 8+) introduz pipelines reativos com tratamento de erros integrado.

// Criação segura de ThreadPoolExecutor customizado
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
ExecutorService executor = new ThreadPoolExecutor(
    10, // core size
    50, // max size
    60L, TimeUnit.SECONDS, // keep alive
    queue,
    new ThreadPoolExecutor.CallerRunsPolicy() // rejeição segura
);
🔧Exemplo

Pipeline Assíncrono Complexo com CompletableFuture

O CompletableFuture permite encadear tarefas concorrentes de forma fluida. O método thenApply transforma dados de forma síncrona; thenCompose atua como flatMap para retornar outro Future; thenCombine junta resultados de dois Futures paralelos; exceptionally intercepta falhas graciosamente.

CompletableFuture.supplyAsync(() -> fetchUserData(userId), executor)
    // pipeline assíncrono encadeado
    .thenApply(user -> user.getPreferences())
    .thenCompose(prefs -> fetchRecommendationsAsync(prefs, executor))
    .thenCombine(fetchPromoBannerAsync(executor), (recs, banner) -> {
        return new PageLayout(recs, banner);
    })
    .exceptionally(ex -> {
        log.error("Erro no fluxo", ex);
        return PageLayout.DEFAULT;
    })
    .thenAccept(layout -> renderPage(layout));
🚽Analogia

O Banheiro Químico vs O Painel Indicador

Synchronized (e os locks do Java) funciona como um banheiro químico de aeroporto: tem um trinco físico (monitor lock). Se alguém entra e fecha o trinco, mais ninguém entra até que saia. Garante exclusão mútua total. Volatile é como o painel luminoso de 'Livre / Ocupado' do avião ligado direto ao trinco. Ele garante que qualquer pessoa no corredor veja o estado correto atualizado instantaneamente (visibilidade de memória), mas não impede que duas pessoas corram em direção à porta ao mesmo tempo e se trombem (falta de exclusão mútua/atomicidade).

💡Conceito

Visibilidade, Atomicidade e Java Memory Model (JMM)

O Java Memory Model define as regras de Happens-before para garantir consistência de memória entre threads. Sem Happens-before, a CPU e o compilador podem reordenar instruções por performance, ou threads podem ler valores defasados dos caches L1/L2 locais de cada núcleo de CPU. • volatile: Garante visibilidade (leituras/escritas vão direto para a memória principal RAM) e impede reordenação de memória, mas NÃO garante atomicidade (ex: count++ ainda sofre de race condition). • Atomic classes (AtomicInteger, etc.): Usam instruções de CPU nativas lock-free como Compare-And-Swap (CAS) para atualizar valores atomicamente sem bloquear threads.

// Usando CAS sob o capô de forma atômica e não bloqueante
AtomicInteger counter = new AtomicInteger(0);
int updatedValue = counter.incrementAndGet(); // Thread-safe sem synchronized
🔧Exemplo

Locks Avançados e Leitura Otimista com StampedLock

Além do synchronized tradicional, o pacote java.util.concurrent.locks fornece ReentrantLock (com controle de timeout via tryLock para evitar deadlocks), ReadWriteLock (múltiplas leituras paralelas, escrita exclusiva) e StampedLock. O StampedLock permite leituras otimistas extremamente rápidas que não travam threads e podem ser validadas posteriormente.

class OptimisticPoint {
    private final StampedLock sl = new StampedLock();
    private double x, y;

    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead(); // sem lock real
        double curX = x, curY = y;
        if (!sl.validate(stamp)) { // valida se houve escrita paralela
            stamp = sl.readLock(); // fallback para lock pessimista
            try {
                curX = x;
                curY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(curX * curX + curY * curY);
    }
}