Back-End

8 abr, 2013

Investigando Deadlocks – Parte 4: consertando o código

Publicidade

No último capítulo desta pequena série de artigos nos quais venho falando sobre análise de deadlocks, vou corrigir o meu código BadTransferOperation. Se você já viu os outros artigos desta série (veja a parte 3 aqui), vai saber que para chegar a este ponto eu criei o código de demonstração desses deadlocks, mostrei como se controla um dump de thread e, em seguida, analisei o dump de thread, descobrindo onde e como um deadlock estava ocorrendo.

A fim de economizar espaço, a discussão a seguir refere-se às classes Account e DeadlockDemo da parte 1 desta série, que contém listagens completa do código.

As descrições acadêmicas de deadlocks geralmente são algo assim: “Thread A vai fazer um bloqueio no objeto 1 e esperar por um bloqueio no objeto 2, enquanto a thread B faz um bloqueio sobre o objeto 2 enquanto espera por um bloqueio no objeto 1”. O engavetamento mostrado no meu artigo anterior, e destacado a seguir, é um deadlock no mundo real, onde outras threads, locks e objetos ficam no caminho direto e simples da situação teórica do deadlock.

Found one Java-level deadlock:
=============================
"Thread-21":
waiting to lock monitor 7f97118bd560 (object 7f3366f58, a threads.deadlock.Account),
which is held by "Thread-20"
"Thread-20":
waiting to lock monitor 7f97118bc108 (object 7f3366e98, a threads.deadlock.Account),
which is held by "Thread-4"
"Thread-4":
waiting to lock monitor 7f9711834360 (object 7f3366e80, a threads.deadlock.Account),
which is held by "Thread-7"
"Thread-7":
waiting to lock monitor 7f97118b9708 (object 7f3366eb0, a threads.deadlock.Account),
which is held by "Thread-11"
"Thread-11":
waiting to lock monitor 7f97118bd560 (object 7f3366f58, a threads.deadlock.Account),
which is held by "Thread-20"

imagem 1

Se você se relaciona o texto e a imagem acima de volta para o código a seguir, você pode ver que Thread-20 bloqueou seu objeto fromAccount (f58) e está esperando para bloquear seu objeto toAccount (e98).

imagem 2

private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {

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

Infelizmente, por questões de tempo, Thread-20 não pode fazer um bloqueio no objeto e98 porque está esperando que Thread-4 libere seu bloqueio naquele objeto. Thread-4 não pode liberar o bloqueio porque está esperando por Thread-7, Thread-7 está esperando por Thread-11 e Thread-11 está esperando Thread-20 liberar o bloqueio no objeto f58. Esse deadlock do mundo real é apenas uma versão mais complicada da descrição acadêmica.

O problema com esse código é que, a partir do trecho abaixo, você pode ver que estou escolhendo aleatoriamente dois objetos da array Account, como o fromAccount e o toAccount, e bloqueá-los. Como fromAccount e toAccount podem fazer referência a qualquer objeto a partir da array de contas, isso significa que eles estão sendo bloqueados em uma ordem aleatória.

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

Portanto, a correção é para impor a ordem de como o objeto Account é bloqueado e qualquer ordem fará, desde que seja consistente.

private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {

      if (fromAccount.getNumber() > toAccount.getNumber()) {

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

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

 

O código acima mostra a correção. Nesse código, estou usando o número da conta para garantir que eu estou bloqueando o objeto Account com o maior número de contas em primeiro lugar, de modo que a situação de deadlock acima nunca aconteça.

O código abaixo é a lista completa para a correção:

public class AvoidsDeadlockDemo {

  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[]) {

    AvoidsDeadlockDemo demo = new AvoidsDeadlockDemo();
    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);
        }
      }
      System.out.println("Thread Complete: " + threadNum);
    }

    private void printNewLine(int columnNumber) {

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

    /**
     * This is the crucial point here. The idea is that to avoid deadlock you need to ensure that threads can't try
     * to lock the same two accounts in the same order
     */
    private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {

      if (fromAccount.getNumber() > toAccount.getNumber()) {

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

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

No meu código de exemplo, um deadlock ocorre por conta de um problema de tempo e as palavras-chave aninhadas synchronized na minha classe BadTransferOperation. Nesse código, as palavras-chave synchronized estão em linhas adjacentes; no entanto, como um ponto final, vale a pena notar que, não importa onde no seu código as palavras-chave synchronized estão (elas não precisam ser adjacentes). Enquanto você está bloqueando dois (ou mais) objetos de monitor diferentes com a mesma thread, então a ordem importa e deadlocks acontecem.

***

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