Caba da TI

Caba da TI

Como evitar problema de concorrência em Java

Neste artigo iremos explicar como pode acontecer um problema de concorrência em uma aplicação e como podemos fazer para evitar tal problema. Tornando portanto uma código thread-safe.

O problema

Em aplicações multithread, pode acontecer de duas ou mais threads acessarem um mesmo método simultaneamente e dependendo do momento em que cada uma executa em cada parte do método, isso pode levar a aplicação a erros inesperados (esse tipo de problema é bem conhecido e recebe o nome de race condition).

Resumindo o problema de forma prática, se dois usuários realizarem uma mesma ação ao mesmo tempo em um sistema e essa ação consequentemente chamar um mesmo método ao mesmo tempo para os dois usuários, dependendo do que é realizado nesse método pode ser que no final da execução do método tenhamos um resultado inesperado.

Talvez o exemplo mais simples de se entender o problema é o tão famigerado exemplo de uma transação bancária. Como faz parte da vida de quase todos e também é um exemplo simples, não tive como fugir de usar o mesmo exemplo.

Vamos supor que você precisa criar uma classe para representar uma conta bancária. Você então cria a classe abaixo. Como você é um programador esperto, você faz uma verificação no método de debitar, para não correr o risco de debitar um valor maior que o saldo e assim deixar o saldo negativo.

public class ContaBancaria {

  private Double saldo;

  public void debitar(Double valorADebitar) {
    if (valorADebitar > 0 && saldo >= valorADebitar) {
      saldo = saldo - valorADebitar;
    }
  }

  public void depositar(Double valorADepositar) {
    saldo = saldo + valorADepositar;
  }

  public Double getSaldo() {
    return saldo;
  }
}

Cenário 1: singlethread (não causa erro)

No primeiro cenário temos uma única thread. Podemos pensar como uma conta bancária onde um único usuário acessará a conta e realizará as seguintes ações:

  1. deposita 100 reais, ficando com saldo de 100 reais
  2. debita 50 reais, ficando com saldo de 50 reais
  3. debita 50 reais, ficando com saldo de 0 reais
  4. tenta debitar mais 50 reais, porém método verifica que o saldo está menor que o valor a sacar, então não realiza a ação
  5. no final temos um comportamento correto do sistema, onde não foi permitido sacar valores que não existiam

Cenário 2: multithread (pode causar erro)

No segundo cenário, podemos supor que a conta é uma conta conjunta, que será acessada pelo esposo (Joaquim) e a esposa (Maria), cada um realizando as seguintes ações:

  1. JOAQUIM: deposita 100 reais, ficando com saldo de 100 reais
  2. JOAQUIM: debita 50 reais, ficando com saldo de 50 reais
  3. JOAQUIM e MARIA: executam a ação de debitar no mesmo instante. Como o ambiente é multithread, as duas threads podem acessar o método simultaneamente. então as duas threads iniciam a execução do método “debitar” nos seguintes passos:
    • THREAD DE JOAQUIM: executa linha 6 (verifica se o saldo é maior ou igual ao valor a debitar), como o saldo ainda é 50 reais, ela “entra” no if
    • THREAD DE JOAQUIM: entra no estado de ESPERA
    • THREAD DE MARIA: executa linha 6 (verifica se o saldo é maior ou igual ao valor a debitar), como o saldo ainda é 50 reais, ela “entra” no if
    • THREAD DE MARIA: executa linha 7 (realiza o débito de 50 reais, ficando com saldo igual a 0)
    • THREAD DE JOAQUIM: executa linha 7 (realiza o débito de 50 reais, como o saldo já era 0, ficará com sado de -50)

O resultado final da execução no cenário 2 foi um valor negativo de -50 reais na conta, ou seja, um valor incorreto causado pelo problema chamado de race condition.

Solução

A solução para esse tipo de problema é sincronizar a parte do código que está causando o erro. Essa sincronização significa permitir que apenas uma thread execute uma parte do código de cada vez, sendo que as outras threads que tentem executar essa parte do código fiquem aguardando a conclusão da execução por parte da primeira thread.

Iremos ver a seguir algumas formas de se conseguir essa sincronização.

Synchronized no método

Uma forma de deixar o código sincronizado é incluir a palavra-chave synchronized no método. Dessa forma o método inteiro ficará sincronizado e apenas uma thread consegue acessá-lo por vez.

public class ContaBancaria {

  private Double saldo;

  public synchronized void debitar(Double valorADebitar) {
    if (valorADebitar > 0 && saldo >= valorADebitar) {
      saldo = saldo - valorADebitar;
    }
  }

  public void depositar(Double valorADepositar) {
    saldo = saldo + valorADepositar;
  }

  public Double getSaldo() {
    return saldo;
  }
}

Cenário 2: multithread, porém com código synchronized (não causa erro)

Agora vamos repetir o segundo cenário, com o método de debitar synchronized. Para relembrar o segundo cenário, ele consiste em uma conta conjunta, onde serão realizados saques simultaneamente por JOAQUIM e MARIA.

  1. JOAQUIM: deposita 100 reais, ficando com saldo de 100 reais
  2. JOAQUIM: debita 50 reais, ficando com saldo de 50 reais
  3. JOAQUIM e MARIA: executam a ação de debitar no mesmo instante. Como o ambiente é multithread e o método é sincronizado, as duas threads podem tentar acessar o método simultaneamente, porém a primeira a acessar obtém o lock do método (chamado de intrinsic lock) e as outras threads ficam aguardando a execução do método pela primeira thread. Veja abaixo o passo-a-passo:
    • THREAD DE JOAQUIM: inicia a execução do método na linha 5 e então obtém o lock do método. Nesse momento nenhuma thread pode acessar o método até que a thread de JOAQUIM conclua a execução do método
    • THREAD DE JOAQUIM: executa linha 6 (verifica se o saldo e maior ou igual ao valor a debitar), como o saldo ainda é 50 reais, ela “entra” no if
    • THREAD DE JOAQUIM: executa linha 7 (realiza o débito de 50 reais, como o saldo já era 0, ficará com sado de 0)
    • THREAD DE JOAQUIM: conclui o método e libera o lock do método, permitindo que outras threads possam acessá-lo
    • THREAD DE MARIA: inicia a execução do método na linha 5 e então obtém o lock do método.
    • THREAD DE MARIA: executa linha 6 (verifica se o saldo e maior ou igual ao valor a debitar), como o saldo é 0, ela “não entra” no if
    • THREAD DE MARIA: conclui o método e libera o lock do método, permitindo que outras threads possam acessá-lo

O resultado final da execução no cenário 2 com método sincronizado foi um valor negativo de 0 reais na conta, ou seja, um valor correto, mesmo com N threads tentando acessar o método ao mesmo tempo.

Synchronized em um bloco de código

Ao sincronizar um método inteiro pode ser criado um gargalo caso o metodo seja muito grande ou muito demorado e acessado por muitas threads simultaneamente. Então nesse caso talvez seja preferível sincronizar somente a parte do código que pode causar problema. Veja abaixo como isso pode ser feito

public class ContaBancaria {

  private Double saldo;

  public void debitar(Double valorADebitar) {
    synchronized (this) {
        if (valorADebitar > 0 && saldo >= valorADebitar) {
          saldo = saldo - valorADebitar;
        }
    }
  }

  public void depositar(Double valorADepositar) {
    saldo = saldo + valorADepositar;
  }

  public Double getSaldo() {
    return saldo;
  }
}

Ao sincronizar um bloco de código, devemos especificar um objeto que será utilizado para obter o lock. Normalmente para isso é utilizado o objeto atual, utilizando-se a palavra-chave this.

Referências

Java Concurrency: Synchronization

Guide to the Synchronized Keyword in Java

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *