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

Что такое гарантированный дедлок?

1.8 Middle🔥 181 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью

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

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

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

Гарантированный дедлок в многопоточности

Гарантированный дедлок (deadlock) — это ситуация в многопоточной программе, когда два или более потока остаются заблокированными навечно, ожидая друг друга в циклической зависимости. Ни один из потоков не может продолжить свою работу, и приложение зависает.

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

Условия возникновения дедлока

Для возникновения дедлока необходимо выполнение всех четырёх условий одновременно:

1. Взаимное исключение (Mutual Exclusion) — ресурс может быть использован только одним потоком одновременно:

private final Object lock = new Object();

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

2. Удержание и ожидание (Hold and Wait) — поток удерживает один ресурс и ждёт другого:

public void transferMoney(Account from, Account to, BigDecimal amount) {
    synchronized(from) { // Удерживаем первый ресурс
        // Пока ждём второй ресурс
        synchronized(to) {
            // Выполняем перевод
        }
    }
}

3. Отсутствие вытеснения (No Preemption) — ресурс нельзя забрать у потока — он может его отпустить только добровольно:

// Когда поток захватил lock, никто не может его отнять
private final Lock lock = new ReentrantLock();
lock.lock(); // Захватили
// Если поток зависнет, lock остаётся захвачен
lock.unlock(); // Только этот поток может отпустить

4. Циклическое ожидание (Circular Wait) — создаётся цикл зависимостей между потоками:

// Поток 1: ждёт B, удерживает A
// Поток 2: ждёт A, удерживает B
// Это образует цикл: A -> B -> A

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

public class DeadlockExample {
    
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();
    
    public static void thread1Method() {
        synchronized(lock1) {
            System.out.println("Поток 1: захватил lock1");
            
            try {
                Thread.sleep(100); // Имитация работы
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            
            System.out.println("Поток 1: ждёт lock2");
            synchronized(lock2) {
                System.out.println("Поток 1: захватил lock2");
            }
        }
    }
    
    public static void thread2Method() {
        synchronized(lock2) {
            System.out.println("Поток 2: захватил lock2");
            
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            
            System.out.println("Поток 2: ждёт lock1");
            synchronized(lock1) {
                System.out.println("Поток 2: захватил lock1");
            }
        }
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(DeadlockExample::thread1Method);
        Thread t2 = new Thread(DeadlockExample::thread2Method);
        
        t1.start();
        t2.start();
        
        // Программа зависнет:
        // Поток 1 захватил lock1 и ждёт lock2
        // Поток 2 захватил lock2 и ждёт lock1
    }
}

Пример дедлока с banco-переводами

Это классическая задача, где дедлоки особенно вероятны:

public class BankAccount {
    private String accountId;
    private BigDecimal balance;
    
    public synchronized void transfer(BankAccount to, BigDecimal amount) {
        if (this.balance.compareTo(amount) >= 0) {
            this.balance = this.balance.subtract(amount);
            to.deposit(amount);
        }
    }
    
    public synchronized void deposit(BigDecimal amount) {
        this.balance = this.balance.add(amount);
    }
}

// Использование:
BankAccount acc1 = new BankAccount("1", new BigDecimal("1000"));
BankAccount acc2 = new BankAccount("2", new BigDecimal("1000"));

Thread t1 = new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        acc1.transfer(acc2, new BigDecimal("10"));
    }
});

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        acc2.transfer(acc1, new BigDecimal("10"));
    }
});

t1.start();
t2.start();
// Высокая вероятность дедлока!

Способы предотвращения дедлока

1. Избегание циклических зависимостей — упорядоченное захватывание ресурсов:

public class OrderedLockTransfer {
    
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();
    
    public static void transfer() {
        // ВСЕГДА захватываем в одном и том же порядке
        synchronized(lock1) {
            synchronized(lock2) {
                System.out.println("Перевод выполнен");
            }
        }
    }
}

2. Использование timeout с Lock:

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

public class TimeoutLockExample {
    
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();
    
    public void safeTransfer() throws InterruptedException {
        lock1.lock();
        try {
            // Пытаемся захватить второй lock с timeout
            if (lock2.tryLock(2, TimeUnit.SECONDS)) {
                try {
                    System.out.println("Оба lock захвачены");
                } finally {
                    lock2.unlock();
                }
            } else {
                System.out.println("Не удалось захватить lock2, отпускаем lock1");
            }
        } finally {
            lock1.unlock();
        }
    }
}

3. Использование CyclicBarrier или других синхронизирующих структур:

import java.util.concurrent.CyclicBarrier;

public class BarrierSynchronization {
    
    private final CyclicBarrier barrier = new CyclicBarrier(2);
    
    public void synchronizedWork() throws Exception {
        // Оба потока встречаются здесь, нет циклического ожидания
        barrier.await();
    }
}

4. Использование высокоуровневых структур (меньше вероятность ошибок):

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    // AtomicInteger использует внутренние механизмы, нет дедлоков
    private final AtomicInteger counter = new AtomicInteger(0);
    
    public void increment() {
        counter.incrementAndGet(); // Безопасно и без дедлоков
    }
}

5. Использование tryLock() с проверкой:

public boolean transferWithRetry(Account from, Account to, BigDecimal amount) 
        throws InterruptedException {
    
    while (true) {
        if (from.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                if (to.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
                    try {
                        from.withdraw(amount);
                        to.deposit(amount);
                        return true;
                    } finally {
                        to.getLock().unlock();
                    }
                }
            } finally {
                from.getLock().unlock();
            }
        }
        // Если не удалось, пробуем ещё раз
        Thread.sleep(10);
    }
}

Обнаружение дедлока

В Java можно обнаружить дедлоки через JMX или дампы потоков:

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
    
    public static void checkForDeadlocks() {
        ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = mxBean.findDeadlockedThreads();
        
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            System.out.println("ОБНАРУЖЕНЫ ДЕДЛОКИ! Потоков: " + deadlockedThreads.length);
            
            var threadInfo = mxBean.getThreadInfo(deadlockedThreads);
            for (var info : threadInfo) {
                System.out.println("Поток: " + info.getThreadName());
                System.out.println("Ожидает: " + info.getLockName());
            }
        }
    }
}

Предотвращение дедлоков требует осторожного проектирования и понимания всех возможных сценариев взаимодействия потоков в приложении.

Что такое гарантированный дедлок? | PrepBro