Не вдаваясь в подробности, потоки могут находиться в одном из нескольких состояний, как показано на диаграмме состояний 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 .