Как можно избежать deadlock?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Избегание deadlock-ов в многопоточных приложениях
Dead lock — один из самых коварных багов. За 10+ лет видел проекты, которые падали в production из-за deadlock-а. Вот проверенные техники.
Что такое deadlock
Классический пример:
Тред 1: Захватил Lock A, хочет захватить Lock B
Тред 2: Захватил Lock B, хочет захватить Lock A
Оба ждут друг друга... DEADLOCK!
public class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("Thread 1: Захватил lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) { // Ждёт lock2
System.out.println("Thread 1: Захватил lock2");
}
}
}
public void method2() {
synchronized (lock2) { // Захватил lock2 первым
System.out.println("Thread 2: Захватил lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) { // Ждёт lock1 - DEADLOCK!
System.out.println("Thread 2: Захватил lock1");
}
}
}
}
Четыре условия deadlock-а (Coffman conditions)
Для deadlock-а нужны ВСЕ 4 условия одновременно:
- Mutual Exclusion — ресурс может использовать только один тред
- Hold and Wait — тред держит ресурс и ждёт другой
- No Preemption — ресурс нельзя отобрать (только добровольно отпустить)
- Circular Wait — циклическая цепочка ожидания
Для избегания достаточно устранить ЛЮБОЕ из них.
1. Избегать вложенных synchronization блоков
❌ Плохо: вложенные блокировки
public void processPayment(Account from, Account to) {
synchronized (from) { // Lock на счёте 1
synchronized (to) { // Lock на счёте 2
transfer(from, to); // Potential deadlock
}
}
}
✅ Хорошо: упорядочить блокировки
public void processPayment(Account from, Account to) {
// Всегда блокируем в одинаковом порядке!
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
transfer(from, to); // No deadlock - всегда одинаковый порядок
}
}
}
2. Использовать ReentrantLock с timeout
Самая надёжная техника — timeout при захвате lock-а.
private final ReentrantLock lock1 = new ReentrantLock();
private final ReentrantLock lock2 = new ReentrantLock();
public void transfer(Account from, Account to) throws InterruptedException {
// Пытаемся захватить с timeout
if (!lock1.tryLock(1, TimeUnit.SECONDS)) {
throw new TimeoutException("Не смог захватить lock1");
}
try {
if (!lock2.tryLock(1, TimeUnit.SECONDS)) {
throw new TimeoutException("Не смог захватить lock2");
}
try {
// Работаем
transfer(from, to);
} finally {
lock2.unlock();
}
} finally {
lock1.unlock();
}
}
Как это помогает:
- Если timeout истёк → освобождаем текущий lock
- Другой тред может продолжить работу
- Deadlock физически невозможен!
3. Использовать concurrent коллекции вместо synchronized
❌ Плохо: ручная синхронизация
private Map<String, Integer> map = new HashMap<>();
public void update(String key, int value) {
synchronized (map) {
int current = map.get(key);
map.put(key, current + value);
}
}
✅ Хорошо: ConcurrentHashMap
private Map<String, Integer> map = new ConcurrentHashMap<>();
public void update(String key, int value) {
map.compute(key, (k, v) -> (v == null) ? value : v + value);
// Внутри используются более тонкие блокировки (segment locking)
// Deadlock минимален
}
4. Использовать immutable объекты
Если объект immutable → нет нужды синхронизировать.
// ❌ Плохо: mutable, нужна синхронизация
public class Account {
private int balance;
public synchronized void transfer(Account to, int amount) {
this.balance -= amount;
to.balance += amount; // Potential deadlock
}
}
// ✅ Хорошо: immutable
public final class Account {
private final int balance;
private final String id;
public Account transfer(int amount) {
return new Account(balance - amount, id); // Новый объект
}
}
// Использование
Account acc1 = new Account(1000, "1");
Account acc2 = new Account(500, "2");
Account newAcc1 = acc1.transfer(100);
Account newAcc2 = acc2.transfer(-100);
// Никаких deadlock-ов!
5. Избегать synchronized на слишком крупное время
Блокируй только критическую секцию.
// ❌ Плохо: долгая блокировка
public synchronized void processOrder(Order order) {
updateDatabase(order); // Может быть медленно
sendEmail(order); // Может быть медленно
logToAnalytics(order); // Может быть медленно
}
// ✅ Хорошо: блокируем только необходимое
public void processOrder(Order order) {
Order validOrder;
synchronized (orders) {
validOrder = orders.get(order.getId()); // Критическая секция
}
// Вне блокировки
updateDatabase(validOrder);
sendEmail(validOrder);
logToAnalytics(validOrder);
}
6. Использовать ReadWriteLock для читающих операций
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int value = 0;
public int getValue() {
lock.readLock().lock(); // Много потоков могут одновременно читать
try {
return value;
} finally {
lock.readLock().unlock();
}
}
public void setValue(int newValue) {
lock.writeLock().lock(); // Только один тред может писать
try {
value = newValue;
} finally {
lock.writeLock().unlock();
}
}
7. Использовать Executors для управления потоками
// ❌ Плохо: создаём потоки вручную, сложнее управлять
Thread t1 = new Thread(() -> { ... });
Thread t2 = new Thread(() -> { ... });
t1.start();
t2.start();
// ✅ Хорошо: ExecutorService контролирует потоки и ресурсы
ExecutorService executor = Executors.newFixedThreadPool(10);
try {
executor.submit(() -> { ... });
executor.submit(() -> { ... });
} finally {
executor.shutdown();
}
8. Обнаружение deadlock-а в production
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
System.err.println("DEADLOCK DETECTED!");
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo info : threadInfos) {
System.err.println(info);
}
}
Чеклист избегания deadlock-ов
✅ Всегда захватывай locks в одинаковом порядке ✅ Используй tryLock() с timeout вместо synchronized ✅ Используй concurrent коллекции (ConcurrentHashMap, CopyOnWriteArrayList) ✅ Минимизируй время в synchronized блоках ✅ Избегай nested synchronized ✅ Используй ReadWriteLock для read-heavy операций ✅ Делай объекты immutable где возможно ✅ Тестируй многопоточность под нагрузкой
Практический пример: Безопасный transfer
public class SafeBank {
private final Map<String, ReentrantLock> accountLocks = new ConcurrentHashMap<>();
private final Map<String, Integer> accounts = new ConcurrentHashMap<>();
public boolean transfer(String from, String to, int amount) throws InterruptedException {
ReentrantLock lockFrom = accountLocks.computeIfAbsent(from, k -> new ReentrantLock());
ReentrantLock lockTo = accountLocks.computeIfAbsent(to, k -> new ReentrantLock());
// Всегда в одинаковом порядке (по имени)
ReentrantLock first = from.compareTo(to) < 0 ? lockFrom : lockTo;
ReentrantLock second = from.compareTo(to) < 0 ? lockTo : lockFrom;
// С timeout
if (!first.tryLock(2, TimeUnit.SECONDS)) return false;
try {
if (!second.tryLock(2, TimeUnit.SECONDS)) return false;
try {
if (accounts.get(from) >= amount) {
accounts.put(from, accounts.get(from) - amount);
accounts.put(to, accounts.get(to) + amount);
return true;
}
return false;
} finally {
second.unlock();
}
} finally {
first.unlock();
}
}
}
Этот код физически не может deadlock-ить.