Приведён фрагмент Java-кода с синхронизацией: synchronized void transfer(Account a, Account b, int sum) { synchronized(a){ synchronized(b){ a.balance -= sum; b.balance += sum; } } }; объясните, как здесь может возникнуть дедлок и предложите изменения дизайна для его предотвращения
Почему дедлок может возникнуть - Два потока делают transfer(A,B) и transfer(B,A). Первый захватывает монитор A, второй — монитор B; затем каждый ждёт второй монитор — классический взаимный блок (deadlock). Краткая диаграмма: - поток1: locked A → пытается locked B - поток2: locked B → пытается locked A Способы предотвращения (с примерами) 1) Жёсткий глобальный порядок захвата блокировок - Всегда захватывать аккаунты в одном и том же порядке (по id или по System.identityHashCode). - Например, если использовать id: сначала блокируем аккаунт с меньшим id. Пример: ```java synchronized void transfer(Account a, Account b, int sum) { if (a.getId() == b.getId()) { // перевод самому себе return; } Object first = (a.getId() < b.getId()) ? a : b; Object second = (a.getId() < b.getId()) ? b : a; synchronized(first) { synchronized(second) { a.balance -= sum; b.balance += sum; } } } ``` (здесь сравнение вида a.getId()<b.getId()a.getId() < b.getId()a.getId()<b.getId()) 2) Использовать java.util.concurrent.locks с tryLock и откатом - Пытаемся захватить обе блокировки с таймаутом; при неудаче — освобождаем и повторяем (backoff). Это предотвращает вечную блокировку. Схема: ```java boolean lockedA = lockA.tryLock(1000, TimeUnit.MILLISECONDS); if (!lockedA) { /* повторить или сообщить об ошибке */ } try { boolean lockedB = lockB.tryLock(1000, TimeUnit.MILLISECONDS); if (!lockedB) { /* отпустить lockA и повторить */ } try { // операция } finally { lockB.unlock(); } } finally { if (lockedA) lockA.unlock(); } ``` (таймауты, например, 100010001000 мс) 3) Один глобальный монитор / менеджер переводов - Синхронизировать на объекте Bank или вести очередь переводов. Простое и надёжное, но снижает параллелизм. 4) Альтернативы высокого уровня - Использовать транзакционные подходы (STM) или атомарные структуры данных. - Хранить баланс в AtomicLong и выполнять корректную логику с CAS — подходит не для всех сценариев. Рекомендация - Простейший и надёжный паттерн: фиксированный порядок захвата (вариант 1). Если важен высокий параллелизм и много конфликтов — использовать tryLock с таймаутом (вариант 2) или дизайн очереди переводов.
- Два потока делают transfer(A,B) и transfer(B,A). Первый захватывает монитор A, второй — монитор B; затем каждый ждёт второй монитор — классический взаимный блок (deadlock).
Краткая диаграмма:
- поток1: locked A → пытается locked B
- поток2: locked B → пытается locked A
Способы предотвращения (с примерами)
1) Жёсткий глобальный порядок захвата блокировок
- Всегда захватывать аккаунты в одном и том же порядке (по id или по System.identityHashCode).
- Например, если использовать id: сначала блокируем аккаунт с меньшим id.
Пример:
```java
synchronized void transfer(Account a, Account b, int sum) {
if (a.getId() == b.getId()) { // перевод самому себе
return;
}
Object first = (a.getId() < b.getId()) ? a : b;
Object second = (a.getId() < b.getId()) ? b : a;
synchronized(first) {
synchronized(second) {
a.balance -= sum;
b.balance += sum;
}
}
}
```
(здесь сравнение вида a.getId()<b.getId()a.getId() < b.getId()a.getId()<b.getId())
2) Использовать java.util.concurrent.locks с tryLock и откатом
- Пытаемся захватить обе блокировки с таймаутом; при неудаче — освобождаем и повторяем (backoff). Это предотвращает вечную блокировку.
Схема:
```java
boolean lockedA = lockA.tryLock(1000, TimeUnit.MILLISECONDS);
if (!lockedA) { /* повторить или сообщить об ошибке */ }
try {
boolean lockedB = lockB.tryLock(1000, TimeUnit.MILLISECONDS);
if (!lockedB) { /* отпустить lockA и повторить */ }
try {
// операция
} finally { lockB.unlock(); }
} finally { if (lockedA) lockA.unlock(); }
```
(таймауты, например, 100010001000 мс)
3) Один глобальный монитор / менеджер переводов
- Синхронизировать на объекте Bank или вести очередь переводов. Простое и надёжное, но снижает параллелизм.
4) Альтернативы высокого уровня
- Использовать транзакционные подходы (STM) или атомарные структуры данных.
- Хранить баланс в AtomicLong и выполнять корректную логику с CAS — подходит не для всех сценариев.
Рекомендация
- Простейший и надёжный паттерн: фиксированный порядок захвата (вариант 1). Если важен высокий параллелизм и много конфликтов — использовать tryLock с таймаутом (вариант 2) или дизайн очереди переводов.