CodeGym /Cursos /JAVA 25 SELF /Condição de corrida (race condition)

Condição de corrida (race condition)

JAVA 25 SELF
Nível 51 , Lição 4
Disponível

1. Conhecendo a condição de corrida (race condition)

Vamos relembrar a condição de corrida (race condition) — situação em que o resultado do programa depende de como as threads obtêm acesso a dados ou recursos compartilhados. Se a ordem de execução muda — o resultado torna-se imprevisível. É como se você e um amigo tentassem editar ao mesmo tempo o mesmo documento: quem escrever mais rápido “vence”, e o texto final pode ficar bem estranho.

Em Java (e em qualquer outra linguagem com suporte a multithreading), uma race condition aparece quando várias threads leem e/ou modificam a mesma variável simultaneamente sem a devida sincronização.

Por que surge uma race condition?

As threads em Java trabalham em paralelo. Se duas threads acessam a mesma variável ao mesmo tempo (por exemplo, aumentam um contador compartilhado), elas podem “atropelar” uma à outra. Mesmo que a operação pareça atômica (por exemplo, counter++), na verdade, não é!

Como funciona counter++?

A operação de incremento envolve várias etapas:

  1. Ler o valor atual da variável na memória.
  2. Aumentar esse valor em uma unidade.
  3. Gravar o novo valor de volta na memória.

Se, nesse momento, outra thread também faz counter++, ambas podem ler o mesmo valor, ambas incrementá-lo e ambas gravar o mesmo resultado — no fim, um incremento “se perde”.

2. Exemplo de race condition: incremento do contador

Vamos escrever um programa simples que inicia várias threads, cada uma das quais incrementa um contador compartilhado em 1. À primeira vista, se executarmos 1000 threads, o valor final do contador deveria ser 1000. Vamos verificar!

public class RaceConditionDemo {
    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        int threads = 1000;
        Thread[] threadArray = new Thread[threads];

        for (int i = 0; i < threads; i++) {
            threadArray[i] = new Thread(() -> {
                counter++; // Operação PERIGOSA!
            });
            threadArray[i].start();
        }

        // Aguardamos a conclusão de todas as threads
        for (int i = 0; i < threads; i++) {
            threadArray[i].join();
        }

        System.out.println("Esperado: " + threads);
        System.out.println("Obtido: " + counter);
    }
}

Saída esperada:

Esperado: 1000
Obtido: 843

O valor pode variar a cada execução: às vezes 900, às vezes 700 e às vezes 1000 — mas muito raramente.

Por que isso acontece?

As threads leem simultaneamente o valor de counter, o incrementam e o gravam de volta. Se duas threads leem o mesmo valor, ambas o incrementam e ambas gravam — um incremento se perde. Como resultado, o valor final é sempre menor que o esperado.

3. Outro exemplo: banco sem sincronização

Vamos imaginar que temos uma conta bancária e duas threads sacam dinheiro ao mesmo tempo.

public class BankAccount {
    private int balance = 100;

    public void withdraw(int amount) {
        if (balance >= amount) {
            // Simulação de trabalho demorado
            try { Thread.sleep(1); } catch (InterruptedException ignored) {}
            balance -= amount;
        }
    }

    public int getBalance() {
        return balance;
    }
}

public class BankDemo {
    public static void main(String[] args) throws InterruptedException {
        BankAccount account = new BankAccount();

        Thread t1 = new Thread(() -> account.withdraw(100));
        Thread t2 = new Thread(() -> account.withdraw(100));

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Esperado: 0 ou 100");
        System.out.println("Saldo efetivo: " + account.getBalance());
    }
}

Às vezes, ambas as threads verão que há 100 na conta, e ambas sacarão o dinheiro. Como resultado, o saldo se tornará -100! (Na vida real isso não acontece, mas em código — facilmente.)

4. Nuances úteis

Consequências da race condition

A condição de corrida não é apenas sobre resultados “estranhos”. É uma verdadeira dor de cabeça para o programador, porque:

  • Os erros nem sempre aparecem. Às vezes o programa funciona corretamente, às vezes não. Tudo depende de como as threads “conseguiram” executar suas ações.
  • Testar não garante sucesso. Você pode executar o programa muitas vezes — e tudo ficar bem, e então, de repente, tudo quebrar.
  • Os erros são difíceis de capturar. O comportamento depende da velocidade do processador, da carga do sistema e de outros programas em execução.
  • Podem ocorrer falhas críticas: perda de dados, cálculos incorretos, queda do aplicativo.

Exemplos reais

  • Aplicações financeiras: cálculo incorreto de saldo, cobranças em dobro.
  • Servidores: perda de mensagens, processamento incorreto de requisições.
  • Jogos: “teleporte” de personagens, pontuações atribuídas de forma errada.

Por que testar não salva da race condition?

Race condition é um típico “Heisenbug” (um bug que desaparece quando você tenta capturá-lo). Mesmo que você rode os testes mil vezes e não veja o erro — isso não significa que ele não existe! Tudo depende de como o sistema operacional irá escalonar o trabalho das threads. Às vezes tudo passa sem problemas, e às vezes as threads “colidem” e o problema surge.

Como evitar a race condition?

  • Sincronização: use a palavra-chave synchronized em métodos ou blocos de código para que apenas uma thread possa modificar os dados compartilhados por vez.
  • Operações atômicas: use classes do pacote java.util.concurrent.atomic (por exemplo, AtomicInteger), que fornecem operações seguras sem sincronização explícita.
  • Imutabilidade: se o objeto não pode ser modificado, a race condition é impossível.

Exemplo com sincronização

public class SafeCounter {
    private int counter = 0;

    public synchronized void increment() {
        counter++;
    }

    public int getValue() {
        return counter;
    }
}

Agora, se várias threads chamarem increment(), apenas uma thread poderá executar esse método em cada momento.

5. Erros comuns ao trabalhar com variáveis compartilhadas entre threads

Erro nº 1: Confiança ingênua na segurança de operações simples.
Muitos pensam que counter++ é uma única operação e nada de ruim pode acontecer. Na verdade, são três operações, e entre elas outra thread pode “se enfiar”.

Erro nº 2: Usar variáveis comuns para troca entre threads.
Se várias threads escrevem e leem a mesma variável sem sincronização — olá, race condition!

Erro nº 3: Esperar que o erro sempre se manifeste.
A race condition pode se manifestar apenas às vezes, o que a torna especialmente traiçoeira. Não confie que, se tudo funcionou nos testes, está tudo bem.

Erro nº 4: Ignorar sincronização ao trabalhar com coleções.
Coleções comuns como ArrayList não são thread-safe. Se várias threads adicionam ou removem elementos — podem ocorrer falhas e até a queda do programa.

Erro nº 5: Tentar “consertar” a race condition com atrasos.
Por exemplo, usando Thread.sleep(10) ou outras pausas “mágicas”. Essa abordagem não resolve o problema; apenas o mascara. A solução real é sincronização ou operações atômicas.

1
Pesquisa/teste
Multithreading, nível 51, lição 4
Indisponível
Multithreading
Fundamentos de multithreading
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION