O Actor Model na JVM: Parte 2 - As Armadilhas do Estado Compartilhado

2 de março de 202520 min de leitura4 views

Mergulho profundo nos problemas específicos que surgem ao lidar com estado mutável compartilhado em ambientes multi-threaded e por que abordagens tradicionais de sincronização falham.

O Actor Model na JVM: Parte 2 - As Armadilhas do Estado Compartilhado
React to this article

Em nosso artigo anterior, exploramos a evolução histórica da Programação Orientada a Objetos e tocamos nos desafios que ela enfrenta em ambientes concorrentes. Hoje, mergulharemos profundamente nas dores de cabeça específicas que emergem quando múltiplas threads interagem com estado mutável compartilhado – problemas que têm atormentado desenvolvedores por décadas.

O Problema Fundamental: Estado Mutável Compartilhado

Imagine Arthur e Maria ambos tentando comprar o último ingresso disponível para um concerto através de uma aplicação web. Ambos clicam em "Comprar Agora" exatamente ao mesmo tempo. O que acontece a seguir ilustra os desafios centrais da programação concorrente:

public class TicketService {
    private int availableTickets = 1; // Apenas um ingresso restante
 
    public boolean purchaseTicket(String customerName) {
        if (availableTickets > 0) {
            // Zona de perigo: Outra thread poderia executar aqui!
            simulateProcessingTime(); // Processamento de cartão de crédito, etc.
            availableTickets--;
            System.out.println(customerName + " successfully purchased a ticket!");
            return true;
        }
        System.out.println("Sorry " + customerName + ", no tickets available.");
        return false;
    }
}

Sem sincronização adequada, tanto Arthur quanto Maria podem ver availableTickets > 0, levando ambos a "com sucesso" comprarem o mesmo ingresso.

Threads Fighting Over Shared Resources
Uma metáfora visual para programação concorrente: múltiplas threads (representadas pelos personagens) lutando por recursos compartilhados (a CPU/chip). Isso ilustra o desafio central do estado mutável compartilhado onde múltiplas threads tentam acessar e modificar o mesmo recurso simultaneamente, levando a conflitos e race conditions.

Os Quatro Cavaleiros da Programação Concorrente

Quando múltiplas threads interagem com estado compartilhado, quatro problemas primários podem surgir, cada um mais insidioso que o anterior.

Diagrama de estado mostrando os quatro modos de falha primários de concorrência e como eles levam a problemas do sistema. O Actor Model elimina essas questões através de passagem de mensagens e processamento sequencial dentro de actors.

1. Race Conditions

Race conditions ocorrem quando o resultado de um programa depende do timing e intercalação de threads. O exemplo do ingresso acima é uma race condition clássica.

// Thread 1: Compra de Arthur
if (availableTickets > 0) { // Lê 1
    // Thread 2 executa aqui e também lê 1
    availableTickets--; // Define para 0
}
 
// Thread 2: Compra de Maria
if (availableTickets > 0) { // Isso era 1 quando verificado!
    availableTickets--; // Define para -1 (!!)
}

O resultado? O sistema pensa que vendeu dois ingressos quando apenas um estava disponível, levando a corrupção de dados e clientes infelizes.

Este diagrama de sequência visualiza o problema exato de timing na race condition. Tanto Arthur quanto Maria verificam disponibilidade antes de qualquer um decrementar, resultando em ambos "com sucesso" comprarem o mesmo ingresso.

Race Condition - The Ticket Vanishes
Este diagrama de sequência mostra uma race condition clássica onde Arthur e Maria ambos verificam disponibilidade de ingressos simultaneamente, ambos veem "1 ingresso disponível", e ambos tentam comprar. O sistema acaba em um estado inconsistente onde ambas as compras têm sucesso mas apenas um ingresso estava realmente disponível.

2. Deadlocks

Deadlocks acontecem quando duas ou mais threads ficam bloqueadas para sempre, esperando uma pela outra para liberar recursos:

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
 
    public void method1() {
        synchronized(lock1) {
            System.out.println("Thread 1: Holding lock1...");
            synchronized(lock2) {
                System.out.println("Thread 1: Holding lock1 & lock2...");
            }
        }
    }
 
    public void method2() {
        synchronized(lock2) {
            System.out.println("Thread 2: Holding lock2...");
            synchronized(lock1) { // Deadlock aqui!
                System.out.println("Thread 2: Holding lock2 & lock1...");
            }
        }
    }
}

Se a Thread 1 adquire lock1 enquanto a Thread 2 adquire lock2, elas esperarão para sempre uma pela outra.

Este diagrama de sequência mostra a dependência circular que cria um deadlock. A Thread 1 mantém lock1 e espera por lock2, enquanto a Thread 2 mantém lock2 e espera por lock1. Nenhuma pode prosseguir.

Ticket Purchase Deadlock Scenario
Este diagrama de sequência ilustra um cenário de deadlock do mundo real onde dois usuários (Arthur e Maria) tentam comprar um ingresso simultaneamente, com o sistema de pagamento e sistema de reserva adquirindo locks em ordens diferentes, resultando em um deadlock.

3. Livelocks

Livelocks são similares a deadlocks, mas as threads não estão bloqueadas – elas estão ativamente tentando resolver o conflito, criando um loop infinito de polidez:

// Duas pessoas em um corredor estreito, ambas se afastando continuamente
public void avoidCollision(Person other) {
    while (isBlocking(other)) {
        moveAside(); // Ambas as pessoas continuam se afastando
        Thread.yield(); // Sendo "educadas"
    }
}

As threads permanecem ativas mas não fazem progresso, como duas pessoas em um corredor ambas se movendo para esquerda e direita em sincronia.

Fluxo de livelock: threads detectam continuamente conflitos e cedem uma à outra, mas ambas tomam a mesma decisão simultaneamente, resultando em um loop infinito de cedência mútua sem progresso.

Livelock - The Endless After You
Este diagrama ilustra um cenário de livelock onde Arthur e Maria ambos estão sendo educados, constantemente dizendo "Depois de você" em um loop infinito. Diferente de um deadlock onde threads estão bloqueadas, em um livelock ambas as threads permanecem ativas mas não fazem progresso, continuamente tentando ceder uma à outra.

4. Starvation

Starvation ocorre quando uma thread é perpetuamente negada acesso a recursos que precisa:

public class PriorityQueue {
    public synchronized void processHighPriority() {
        // Tarefas de alta prioridade continuam sendo processadas
        while (hasHighPriorityTasks()) {
            processNext();
        }
    }
 
    public synchronized void processLowPriority() {
        // Isso pode nunca executar se tarefas de alta prioridade continuarem chegando!
        processNext();
    }
}
Starvation - The VIP Line
Este diagrama demonstra starvation de thread onde clientes VIP continuamente obtêm acesso prioritário a recursos, enquanto usuários regulares (Arthur) podem nunca ter uma chance de completar suas transações, apesar de estarem ativos no sistema.

Threads de baixa prioridade podem esperar indefinidamente enquanto threads de alta prioridade continuamente pegam recursos.

Soluções Tradicionais e Suas Limitações

Java fornece vários mecanismos para lidar com essas questões, mas cada um vem com trade-offs:

Palavras-chave Synchronized

public synchronized boolean purchaseTicket(String customerName) {
    // Thread-safe, mas cria um gargalo
    // Apenas uma thread pode executar este método por vez
}

Prós: Simples, previne race conditions Contras: Escalabilidade ruim, potencial para deadlocks

Campos Volatile

private volatile boolean isAvailable = true;

Prós: Garante visibilidade de mudanças entre threads Contras: Funciona apenas para operações únicas, não previne race conditions em operações complexas

Classes Atômicas

private AtomicInteger availableTickets = new AtomicInteger(1);
 
public boolean purchaseTicket(String customerName) {
    if (availableTickets.getAndDecrement() > 0) {
        return true;
    }
    availableTickets.incrementAndGet(); // Rollback
    return false;
}

Prós: Melhor performance que synchronized Contras: Limitado a operações simples, não resolve coordenação complexa

O Dilema do Cache

Aplicações modernas frequentemente adicionam camadas de cache para performance, o que introduz complexidade adicional:

public class CachedTicketService {
    private final Map<String, Integer> cache = new ConcurrentHashMap<>();
    private final Database database;
 
    public boolean purchaseTicket(String event) {
        Integer cached = cache.get(event);
        if (cached != null && cached > 0) {
            // Cache hit, mas esses dados ainda são válidos?
            // Outra instância pode ter vendido ingressos!
            return processPurchase(event);
        }
        // Cache miss, verificar banco de dados
        return checkDatabaseAndPurchase(event);
    }
}

Agora temos que nos preocupar com invalidação de cache entre múltiplas instâncias, consistência entre cache e banco de dados, e locking distribuído em ambientes clusterizados.

Por Que OOP Luta com Concorrência

O princípio central da Programação Orientada a Objetos de encapsulamento assume que objetos podem proteger seu estado interno. Mas quando múltiplas threads acessam o mesmo objeto, essa proteção se quebra:

  1. Encapsulamento não é suficiente: Campos privados não protegem contra acesso concorrente
  2. Sincronização em nível de método é muito grosseira: Cria gargalos desnecessários
  3. Grafos de objetos complexos requerem locking complexo: Levando a riscos de deadlock
  4. Herança complica thread safety: Subclasses podem quebrar suposições da classe pai

O Problema do Modelo Mental

Talvez o maior desafio seja que estado mutável compartilhado requer que desenvolvedores pensem sobre todas as possíveis intercalações de execução de threads. Isso rapidamente se torna mentalmente esmagador:

  • Com 2 threads e 3 operações cada, há 20 ordens de execução possíveis
  • Com 3 threads e 4 operações cada, há 369.600 ordens de execução possíveis
  • Com aplicações realistas tendo centenas de threads... a complexidade explode

Um Caminho Diferente à Frente

O Actor Model aborda esses problemas eliminando estado mutável compartilhado completamente. Ao invés de múltiplas threads acessando os mesmos dados, cada actor possui seu estado completamente e a comunicação acontece apenas através de mensagens. Isso elimina locks, race conditions e deadlocks inteiramente, enquanto fornece isolamento de falhas natural e supervisão.

Considere como nosso serviço de ingressos poderia parecer com actors:

// Abordagem conceitual baseada em Actor
public class TicketActor extends AbstractActor {
    private int availableTickets = 1;
 
    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(PurchaseRequest.class, this::handlePurchase)
            .build();
    }
 
    private void handlePurchase(PurchaseRequest request) {
        if (availableTickets > 0) {
            availableTickets--;
            getSender().tell(new PurchaseSuccess(), getSelf());
        } else {
            getSender().tell(new PurchaseFailure("No tickets available"), getSelf());
        }
    }
}

Sem palavras-chave de sincronização, sem locks, sem race conditions – apenas processamento simples e sequencial de mensagens.

Olhando para Frente

Os problemas que exploramos hoje – race conditions, deadlocks, livelocks e starvation – têm atormentado programação concorrente por décadas. Soluções tradicionais de OOP, embora funcionais, frequentemente criam mais complexidade do que resolvem.

Em nosso próximo artigo, exploraremos como o Actor Model fornece soluções elegantes para esses desafios e mergulharemos em padrões de implementação prática usando Akka e Apache Pekko na JVM.

A jornada do estado mutável compartilhado para arquiteturas de passagem de mensagens não é apenas sobre evitar bugs – é sobre construir sistemas que são inerentemente mais escaláveis, manuteníveis e resilientes a falhas.


Próximo na Parte 3: Implementaremos um sistema completo baseado em Actor, exploraremos estratégias de supervisão e veremos como passagem de mensagens elimina as armadilhas de concorrência que discutimos hoje.

Arthur CostaA

Arthur Costa

Senior Full-Stack Engineer & Tech Lead

Senior Full-Stack Engineer with 8+ years in React, TypeScript, and Node.js. Expert in performance optimization and leading engineering teams.

View all articles →