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





