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"
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).
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





