Back-End

14 mar, 2013

Investigando Deadlocks – Parte 1: criando um Deadlock

Publicidade

Tenho certeza de que todos nós já passamos por essa situação: está tarde, você está com fome, o servidor está ocupado ou o seu aplicativo está funcionando em passo de lesma, e tem alguém respirando no seu cangote querendo que você corrija o problema antes que você vá embora. Uma das possíveis causas de o seu aplicativo estar fechando de forma inesperada é um problema de thread, conhecido como deadlock.

Sem entrar em muitos detalhes, as threads podem estar em um de uma série de estados, como mostrado pelo diagrama de estado UML abaixo…

Thread States

… e um deadlock tem tudo a ver com o estado BLOCKED, que a documentação da API define como “uma thread que está bloqueada à espera de um monitor de bloqueio”.

Então, o que é um deadlock? De forma simples: dadas duas threads A e B, um deadlock ocorre quando a thread A bloqueia porque está esperando a thread B liberar um monitor de bloqueio, e a thread B bloqueia, pois está esperando a thread A liberar o mesmo monitor de bloqueio.

No entanto, as coisas podem ser mais complexas do que isso, caso o dreadlock tenha um monte de threads. Por exemplo, a thread A bloqueia porque está esperando a thread B, a thread B também bloqueia pois está esperando pela thread C, que por sua vez bloqueia por estar esperando pela thread D, que bloqueia porque está esperando a E, que bloqueia porque está à espera de F que, por fim, bloqueia porque está à espera de A.

O truque é descobrir quais threads estão bloqueadas e por quê. Isso é feito pegando um dump da thread do seu aplicativo. Um dump da thread é simplesmente um relatório instantâneo que mostra o status de todas as threads do seu aplicativo em um determinado ponto no tempo. Existem várias ferramentas e técnicas disponíveis para ajudar você a arranjar um dump da thread, e isso inclui jVisualVM, jstack e o comando unix kill; no entanto, antes de obter e interpretar um dump da thread, vou precisar de algum código que vai criar um deadlock.

O cenário que eu escolhi para isso é de uma simples transferência de conta bancária. A ideia é a de que existe um equilíbrio do programa de balanceamento de transferência que está transferindo aleatoriamente várias quantidades entre contas diferentes, utilizando várias threads. Neste programa, uma conta bancária é representada usando a classe Account:

public class Account {

  private final int number;

  private int balance;

  public Account(int number, int openingBalance) {
    this.number = number;
    this.balance = openingBalance;
  }

  public void withdraw(int amount) throws OverdrawnException {

    if (amount > balance) {
      throw new OverdrawnException();
    }

    balance -= amount;
  }

  public void deposit(int amount) {

    balance += amount;
  }

  public int getNumber() {
    return number;
  }

  public int getBalance() {
    return balance;
  }
}

A classe acima molda uma conta bancária com atributos de número de conta e equilíbrio, e operações como deposit(…)withdraw(…). withdraw(…) vão lançar uma exceção verificada simples, OverdrawnException, se o montante a ser sacado for superior ao saldo disponível.

As demais classes no código de exemplo são DeadlockDemo e sua classe aninhada BadTransferOperation.

public class DeadlockDemo {

  private static final int NUM_ACCOUNTS = 10;
  private static final int NUM_THREADS = 20;
  private static final int NUM_ITERATIONS = 100000;
  private static final int MAX_COLUMNS = 60;

  static final Random rnd = new Random();

  List<Account> accounts = new ArrayList<Account>();

  public static void main(String args[]) {

    DeadlockDemo demo = new DeadlockDemo();
    demo.setUp();
    demo.run();
  }

  void setUp() {

    for (int i = 0; i < NUM_ACCOUNTS; i++) {
      Account account = new Account(i, rnd.nextInt(1000));
      accounts.add(account);
    }
  }

  void run() {

    for (int i = 0; i < NUM_THREADS; i++) {
      new BadTransferOperation(i).start();
    }
  }

  class BadTransferOperation extends Thread {

    int threadNum;

    BadTransferOperation(int threadNum) {
      this.threadNum = threadNum;
    }

    @Override
    public void run() {

      for (int i = 0; i < NUM_ITERATIONS; i++) {

        Account toAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
        Account fromAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
        int amount = rnd.nextInt(1000);

        if (!toAccount.equals(fromAccount)) {
          try {
            transfer(fromAccount, toAccount, amount);
            System.out.print(".");
          } catch (OverdrawnException e) {
            System.out.print("-");
          }

          printNewLine(i);
        }
      }
      // This will never get to here...
      System.out.println("Thread Complete: " + threadNum);
    }

    private void printNewLine(int columnNumber) {

      if (columnNumber % MAX_COLUMNS == 0) {
        System.out.print("\n");
      }
    }

    /**
     * The clue to spotting deadlocks is in the nested locking - synchronized keywords. Note that the locks DON'T
     * have to be next to each other to be nested.
     */
    private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {

      synchronized (fromAccount) {
        synchronized (toAccount) {
          fromAccount.withdraw(transferAmount);
          toAccount.deposit(transferAmount);
        }
      }
    }
  }
}

DeadlockDemo fornece o framework do aplicativo que cria um dreadlock. Ele tem duas tarefas simples: setup() e run(). setup() cria 10 contas inicializando-as com um número de conta e um saldo de abertura aleatória. run() cria 20 instâncias da classe aninhada BadTransferOperation, que simplesmente estende Thread e faz com que eles comecem a funcionar. Observe que os valores utilizados para o número de threads e contas são totalmente arbitrários.

BadTransferOperation é onde toda a ação acontece. O seu método run() faz um loop de 10000 vezes escolhendo aleatoriamente duas contas da lista account e transferindo um valor aleatório entre 0 e 1000 de uma para a outra. Se o fromAccount contém fundos insuficientes, então uma exceção é lançada e um ‘-‘ impresso na tela. Se tudo correr bem e a transferência for bem sucedida, então um ‘.’ é impresso na tela.

O cerne da questão é o método transfer(Account fromAccount, Account toAccount, int transferAmount) contendo o código de sincronização FAULTY:

 

     synchronized (fromAccount) {
        synchronized (toAccount) {
          fromAccount.withdraw(transferAmount);
          toAccount.deposit(transferAmount);
        }
      }

Esse primeiro código bloqueia o fromAccount, e em seguida o toAccount antes de transferir o dinheiro e posteriormente liberar ambos os bloqueios.

Dados duas threads A e B e contas 1 e 2, então os problemas surgirão quando a thread A bloqueia o seu fromAccount, número 1, e tentar bloquear seu toAccount, que é a conta número 2. Simultaneamente, a thread B bloqueia sua  fromAccount, número 2, e tenta bloquear seu toAccount, que é a conta  número 1. Assim, a thread A está BLOCKED na thread B, e a thread B está bloqueada no thread A – um deadlock.

Se você executar esse aplicativo, então terá algum resultado parecido com isto:

Screen Shot

… como o programa vem de uma parada abrupta.

Agora eu tenho um aplicativo deadlocked, o meu próximo artigo vai realmente se apossar de um dump de thread e dar uma olhada no que tudo isso significa.

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.captaindebug.com/2012/10/investigating-deadlocks-part-1.html#.UR-Ra6U3uSq