Статьи

Исследование тупиков — Часть 4. Исправление кода

В последнем из этой небольшой серии блогов, в которых я говорил об анализе взаимоблокировок, я собираюсь исправить свой код BadTransferOperation . Если вы видели другие блоги в этой серии , вы будете знать, что для этого я создал демо-код, который блокирует блокировку, показал, как получить дамп потока, а затем проанализировал дамп потока, выяснить, где и как возникла тупиковая ситуация. В целях экономии места в приведенном ниже обсуждении используются классы Account и DeadlockDemo из первой части этой серии , в которой содержатся полные списки кодов.

Описание тупиковых ситуаций в учебниках обычно выглядит примерно так: «Поток A получит блокировку на объекте 1 и будет ожидать блокировки на объекте 2, тогда как поток B получит блокировку на объекте 2, ожидая блокировки на объекте 1». Скопление, показанное в моем предыдущем блоге и выделенное ниже, является реальной тупиковой ситуацией, когда другие потоки, блокировки и объекты мешают прямой, простой, теоретической тупиковой ситуации.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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'

Если вы свяжете текст и изображение выше со следующим кодом, вы увидите, что Thread-20 заблокировал свой объект fromAccount (f58) и ожидает блокировки своего объекта toAccount (e98)

1
2
3
4
5
6
7
8
9
private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {
 
      synchronized (fromAccount) {
        synchronized (toAccount) {
          fromAccount.withdraw(transferAmount);
          toAccount.deposit(transferAmount);
        }
      }
    }

К сожалению, из-за проблем с синхронизацией, Thread-20 не может получить блокировку для объекта e98, потому что он ожидает, пока Thread-4 освободит свою блокировку для этого объекта. Thread-4 не может снять блокировку, потому что он ожидает Thread-7 , Thread-7 ожидает Thread-11 а Thread-11 ожидает, пока Поток Thread-20 снимет свою блокировку на объекте f58. Этот реальный тупик — просто более сложная версия описания учебника.

Проблема с этим кодом заключается в том, что из приведенного ниже фрагмента видно, что я случайно выбираю два объекта Account из массива Accounts в качестве fromAccount и toAccount и блокирую их. Поскольку fromAccount и toAccount могут ссылаться на любой объект из массива toAccount , это означает, что они заблокированы в случайном порядке.

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

Следовательно, исправление состоит в том, чтобы навести порядок блокирования объекта Account и выполнения любого ордера, если он согласован.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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);
         }
       }
     }
   }

Код выше показывает исправление. В этом коде я использую номер Account записи, чтобы сначала заблокировать объект Account с наибольшим номером учетной записи, чтобы вышеописанная ситуация взаимоблокировки никогда не возникала.

Код ниже — это полный список исправлений:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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);
          }
        }
      }
    }
  }
}

В моем примере кода возникает тупик из-за проблемы синхронизации и вложенных synchronized ключевых слов в моем классе BadTransferOperation . В этом коде synchronized ключевые слова находятся на соседних строках; однако, в заключение, стоит отметить, что не имеет значения, где в вашем коде synchronized ключевые слова (они не обязательно должны быть смежными). Пока вы блокируете два (или более) разных объекта монитора одним и тем же потоком, упорядочение имеет место и возникают взаимные блокировки.

Для получения дополнительной информации см. Другие блоги в этой серии .

Весь исходный код для этого и других блогов из этой серии доступен на Github по адресу: gitub.com/roghughe/captaindebug.git.

Ссылка: Исследование тупиков — Часть 4. Исправление кода от нашего партнера по JCG Роджера Хьюза в блоге Captain Debug’s Blog .