Статьи

Расследование тупиковых ситуаций — часть 1

Я уверен, что мы все были там: уже поздно, вы голодны, ваш сервер завис или ваше приложение работает со скоростью улитки, и кто-то дышит вам в шею, желая, чтобы вы решили проблему до того, как вы уйдете. Одной из возможных причин неожиданного зависания вашего приложения является проблема с потоками, известная как тупик .

Не вдаваясь в подробности, потоки могут находиться в одном из нескольких состояний, как показано на диаграмме состояний UML ниже…

… И взаимоблокировка — это все, что связано с состоянием BLOCKED, которое в документации API определяется как «поток, который заблокирован в ожидании блокировки монитора».

Итак, что такое тупик? Проще говоря, при наличии двух потоков A и B возникает тупик, когда поток A блокирует, потому что он ожидает, пока поток B освободит блокировку монитора, и поток B блокирует, потому что он ожидает, что поток A снимет такую ​​же блокировку монитора.

Тем не менее, все может быть более сложным, чем это в том, что тупик может содержать целую кучу потоков. Например, поток A блокирует, потому что он ожидает поток B, поток B блокирует, потому что он ожидает поток C, поток C блокирует, потому что он ожидает блоки D, D, потому что он ждет блоки E, E, потому что он ждет F и F блоки, потому что он ждет А.

Хитрость заключается в том, чтобы выяснить, какие потоки заблокированы и почему, и это делается путем получения дампа потоков из вашего приложения. Дамп потока — это просто отчет о снимке, показывающий состояние всех потоков вашего приложения в данный момент времени. Есть несколько инструментов и методов, которые помогут вам получить дамп потока, включая jVisualVM , jstack и команду unix kill ; однако перед получением и интерпретацией дампа потока мне понадобится код, который создаст тупик

Сценарий, который я выбрал для этого, представляет собой простой банковский перевод. Идея состоит в том, что существует программа переноса баланса, которая случайным образом переводит различные суммы между разными учетными записями, используя несколько потоков. В этой программе банковский счет представлен с использованием следующего, очень упрощенного класса 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
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;
  }
}

Приведенный выше класс моделирует банковский счет с атрибутами номера счета и баланса, а также такими операциями, как deposit(...) и withdraw(...) . withdraw(...) вызовет простое проверенное исключение OverdrawnException , если сумма вывода превышает доступный остаток.

Остальные классы в примере кода — это DeadlockDemo и его вложенный класс BadTransferOperation .

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
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 предоставляет каркас приложения, который создает тупик. У него две простые задачи: setup() и run() . setup() создает 10 учетных записей, инициализируя их номером счета и случайным начальным балансом. run() создает 20 экземпляров вложенного класса BadTransferOperation , который просто расширяет Thread и запускает их выполнение. Обратите внимание, что значения, используемые для количества потоков и учетных записей, являются абсолютно произвольными.

BadTransferOperation — то, где все действие имеет место. Его метод run() зацикливает 10000 раз случайным образом, выбирая две учетные записи из списка accounts и передавая случайную сумму от 0 до 1000 от одной к другой. Если в fromAccount недостаточно средств, возникает исключение и на экране fromAccount знак «-». Если все прошло хорошо и передача прошла успешно, то «.» печатается на экране.

Суть вопроса — метод transfer(Account fromAccount, Account toAccount, int transferAmount) содержащий код синхронизации FAULTY :

1
2
3
4
5
6
synchronized (fromAccount) {
        synchronized (toAccount) {
          fromAccount.withdraw(transferAmount);
          toAccount.deposit(transferAmount);
        }
      }

Этот код сначала блокирует fromAccount , а затем toAccount прежде чем переводить деньги и впоследствии toAccount обе блокировки.

Учитывая два потока A и B и учетные записи 1 и 2, тогда возникнут проблемы, когда поток A заблокирует свой fromAccount , номер 1, и попытается заблокировать свой toAccount , который является номером счета 2. Одновременно поток B блокирует свой fromAccount , номер 2, и пытается заблокировать свой toAccount , который является номером счета 1. Следовательно, поток A заблокирован в потоке B, а поток B заблокирован в потоке A — тупик.

Если вы запустите это приложение, вы получите вывод, который выглядит примерно так:

… поскольку программа внезапно останавливается.

Теперь у меня есть заблокированное приложение, мой следующий блог фактически получит дамп потока и рассмотрит, что все это значит.

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