← Назад к вопросам

Что такое deadlock (взаимная блокировка)? Как его избежать?

2.2 Middle🔥 161 комментариев
#JVM и управление памятью#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Deadlock (взаимная блокировка) в Java

Определение Deadlock

Deadlock — это ситуация, когда два или более потока ждут друг друга бесконечно и ни один из них не может продолжить работу.

Поток 1: ждёт ресурс B (занят Потоком 2)
↓
Поток 2: ждёт ресурс A (занят Потоком 1)
↓
Ни один поток не может двигаться → DEADLOCK

Классический пример Deadlock

public class DeadlockExample {
    
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();
    
    // Поток 1
    public static void method1() throws InterruptedException {
        synchronized (lock1) {  // Захватил lock1
            System.out.println("Thread 1: захватил lock1");
            
            Thread.sleep(1000); // Имитация работы
            
            synchronized (lock2) { // Ждёт lock2 (занят Потоком 2)
                System.out.println("Thread 1: захватил lock2");
            }
        }
    }
    
    // Поток 2
    public static void method2() throws InterruptedException {
        synchronized (lock2) {  // Захватил lock2
            System.out.println("Thread 2: захватил lock2");
            
            Thread.sleep(1000); // Имитация работы
            
            synchronized (lock1) { // Ждёт lock1 (занят Потоком 1)
                System.out.println("Thread 2: захватил lock1");
            }
        }
    }
    
    public static void main(String[] args) {
        new Thread(() -> {
            try { method1(); } catch (InterruptedException e) { }
        }).start();
        
        new Thread(() -> {
            try { method2(); } catch (InterruptedException e) { }
        }).start();
        
        // Результат: DEADLOCK!
        // Thread 1 ждёт lock2, Thread 2 ждёт lock1
        // Программа зависает навечно
    }
}

Условия для Deadlock (все 4 должны быть истинны)

1. Mutual Exclusion (Взаимное исключение)

// Ресурс может использоваться только одним потоком
synchronized (lock) {
    // Только один поток может быть здесь
}

2. Hold and Wait (Удержание и ожидание)

synchronized (lock1) {      // Удерживаем lock1
    // ...
    synchronized (lock2) {   // И ждём lock2
        // Deadlock возможен если другой поток делает наоборот
    }
}

3. No Preemption (Нет вытеснения)

// Нельзя насильно отобрать ресурс у потока
// Поток должен сам отпустить lock
synchronized (lock) {
    // Только сам поток может отпустить lock
}

4. Circular Wait (Циклическое ожидание)

Поток 1: lock1 → lock2 → lock3 → lock1 (цикл!)
Поток 2: lock2 → lock3 → lock1 → lock2 (цикл!)

Как избежать Deadlock

1. Фиксированный порядок захвата ресурсов (BEST PRACTICE)

Всегда захватывайте ресурсы в одном и том же порядке:

public class NoDeadlockExample {
    
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();
    
    // Всегда: сначала lock1, потом lock2
    public static void method1() throws InterruptedException {
        synchronized (lock1) {  // ✓ Первый
            Thread.sleep(1000);
            synchronized (lock2) { // ✓ Второй
                System.out.println("Method 1 успешно");
            }
        }
    }
    
    public static void method2() throws InterruptedException {
        synchronized (lock1) {  // ✓ Первый (тот же порядок!)
            Thread.sleep(1000);
            synchronized (lock2) { // ✓ Второй (тот же порядок!)
                System.out.println("Method 2 успешно");
            }
        }
    }
    
    // Результат: NO DEADLOCK ✓
}

Почему это работает:

Поток 1: захватил lock1 → ждёт lock2
Поток 2: ждёт lock1 (захватит, когда Поток 1 отпустит)
        → потом ждёт lock2

Без цикла, deadlock невозможен!

2. Таймауты (Timeout) вместо вечного ожидания

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class DeadlockWithTimeout {
    
    private ReentrantLock lock1 = new ReentrantLock();
    private ReentrantLock lock2 = new ReentrantLock();
    
    public boolean transferMoney() throws InterruptedException {
        boolean acquired1 = false, acquired2 = false;
        
        try {
            // Пытаемся захватить с таймаутом (2 секунды)
            acquired1 = lock1.tryLock(2, TimeUnit.SECONDS);
            if (!acquired1) return false; // Не получили lock1
            
            acquired2 = lock2.tryLock(2, TimeUnit.SECONDS);
            if (!acquired2) return false; // Не получили lock2
            
            // Оба локка захвачены, выполняем операцию
            System.out.println("Транзакция успешна");
            return true;
            
        } finally {
            // Гарантированно отпускаем ресурсы
            if (acquired2) lock2.unlock();
            if (acquired1) lock1.unlock();
        }
    }
}

// Использование
DeadlockWithTimeout example = new DeadlockWithTimeout();
for (int i = 0; i < 5; i++) {
    if (!example.transferMoney()) {
        System.out.println("Deadlock обнаружен, повторная попытка...");
        Thread.sleep(100); // Подождали и попробовали снова
    }
}

3. ReadWriteLock для уменьшения конкурентности

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private int value = 0;
    
    // Множество читателей одновременно
    public int read() {
        rwLock.readLock().lock();
        try {
            return value;
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    // Писатель блокирует всех
    public void write(int newValue) {
        rwLock.writeLock().lock();
        try {
            value = newValue;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

// Результат: меньше блокировок, меньше шансов на deadlock

4. Использование ConcurrentCollections

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentExample {
    
    // ❌ Может быть deadlock
    private Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
    
    // ✓ Нет deadlock (internal locking более продвинуто)
    private ConcurrentMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
    
    public void process() {
        // ConcurrentHashMap использует segment-based locking
        // Не блокирует весь map, только один сегмент
        concurrentMap.put("key", 100);
        concurrentMap.get("key");
    }
}

5. StampedLock (Java 8+) для высокой производительности

import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
    
    private double x, y;
    private final StampedLock sl = new StampedLock();
    
    public double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead(); // Оптимистичное чтение
        double currentX = x;
        double currentY = y;
        
        // Если данные изменились во время чтения
        if (!sl.validate(stamp)) {
            // Перечитываем с полной блокировкой
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
    
    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
}

6. Actor Model (Akka) для избежания синхронизации

// Вместо shared state + синхронизации
// используем message passing (Akka Actors)

// Каждый Actor имеет свой mailbox
// Сообщения обрабатываются последовательно
// Нет shared state → нет deadlock

Detecting Deadlock (обнаружение)

// 1. JVM предоставляет инструменты
jps -l           // Список Java процессов
jstack <pid>    // Dump потоков и locks

// 2. В jstack выглядит так:
// "Thread-1": waiting to lock monitor 0x...
// "Thread-0": holding lock 0x...
// → DEADLOCK обнаружен!

Чеклист для избежания Deadlock

✓ Захватывайте ресурсы в одном и том же порядке ✓ Используйте таймауты (tryLock вместо lock) ✓ Минимизируйте время удержания ресурсов ✓ Используйте ConcurrentCollections вместо synchronized ✓ Предпочитайте immutable объекты ✓ Логируйте и мониторьте потоки ✓ Используйте tool'ы (JProfiler, YourKit) для обнаружения

Реальный пример: правильная банковская транзакция

@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
    // Правило: всегда сначала ID с меньшим значением
    if (from.getId() < to.getId()) {
        synchronized (from) {
            synchronized (to) {
                from.debit(amount);
                to.credit(amount);
            }
        }
    } else {
        synchronized (to) {
            synchronized (from) {
                from.debit(amount);
                to.credit(amount);
            }
        }
    }
    // NO DEADLOCK ✓
}

Вывод

Deadlock — это серьёзная проблема, но она полностью избежима:

  1. Основное правило: захватывайте ресурсы в одинаковом порядке
  2. Таймауты: используйте tryLock() вместо lock()
  3. Concurrent коллекции: ConcurrentHashMap вместо synchronized
  4. Минимизируйте синхронизацию: используйте immutable объекты
  5. Мониторьте: используйте tools для обнаружения deadlocks

В современной Java (Stream API, CompletableFuture, reactive frameworks) часто вообще избегают необходимости явной синхронизации, что естественным образом предотвращает deadlocks.

Что такое deadlock (взаимная блокировка)? Как его избежать? | PrepBro