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…
… 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(…) e 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:
… 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