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

Deadlock: воспроизведение и решение

1.7 Middle🔥 171 комментариев
#Многопоточность#Основы Java

Условие

  1. Напишите код, который гарантированно создаёт deadlock
  2. Объясните, почему возникает deadlock
  3. Предложите способы избежать deadlock

Пример ситуации

Два потока пытаются захватить два ресурса (lock1 и lock2), но в разном порядке:

  • Поток 1: lock1 → lock2
  • Поток 2: lock2 → lock1

Требования

  • Продемонстрируйте deadlock
  • Используйте synchronized или ReentrantLock
  • Покажите решение с упорядочиванием блокировок
  • Объясните, как обнаружить deadlock (jstack, ThreadMXBean)

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

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

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

Deadlock: воспроизведение и решение

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

1. Воспроизведение Deadlock

Классический пример с двумя потоками

public class DeadlockDemo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();
    
    public static void main(String[] args) {
        // Поток 1: захватывает lock1, потом пытается захватить lock2
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("[Thread-1] Захватил lock1");
                sleep(100);  // Даём возможность thread2 захватить lock2
                
                System.out.println("[Thread-1] Пытаюсь захватить lock2...");
                synchronized (lock2) {
                    System.out.println("[Thread-1] Захватил lock2");
                }
            }
        }, "Thread-1");
        
        // Поток 2: захватывает lock2, потом пытается захватить lock1
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("[Thread-2] Захватил lock2");
                sleep(100);  // Даём возможность thread1 захватить lock1
                
                System.out.println("[Thread-2] Пытаюсь захватить lock1...");
                synchronized (lock1) {
                    System.out.println("[Thread-2] Захватил lock1");
                }
            }
        }, "Thread-2");
        
        thread1.start();
        thread2.start();
        
        // Ждём завершения (оно никогда не произойдёт!)
        try {
            thread1.join(2000);  // Timeout 2 сек
            thread2.join(2000);
            
            if (thread1.isAlive() || thread2.isAlive()) {
                System.out.println("\n⚠ DEADLOCK ОБНАРУЖЕН! Потоки зависли.");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Вывод:

[Thread-1] Захватил lock1
[Thread-2] Захватил lock2
[Thread-1] Пытаюсь захватить lock2...
[Thread-2] Пытаюсь захватить lock1...

⚠ DEADLOCK ОБНАРУЖЕН! Потоки зависли.

2. Почему возникает deadlock?

Четыре необходимых условия:

  1. Взаимное исключение — ресурс может использовать только один поток
  2. Удержание и ожидание — поток удерживает ресурс и ждёт другого
  3. Отсутствие вытеснения — поток не может забрать ресурс у другого
  4. Циклическое ожидание — круговая цепочка потоков

В нашем примере:

Поток 1 → [lock1] → ждёт [lock2]
                       ↑
Поток 2 → [lock2] → ждёт [lock1]
                       ↑
Циклическое ожидание! Deadlock.

3. Решение 1: Упорядочивание блокировок

Главный принцип: все потоки должны захватывать ресурсы в одинаковом порядке.

public class DeadlockFixed_Ordering {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();
    
    public static void main(String[] args) {
        // Поток 1: lock1 → lock2 (порядок A)
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("[Thread-1] Захватил lock1");
                sleep(100);
                synchronized (lock2) {
                    System.out.println("[Thread-1] Захватил lock2 - OK!");
                }
            }
        }, "Thread-1");
        
        // Поток 2: ТОЖЕ lock1 → lock2 (порядок A)
        // ВМЕСТО lock2 → lock1!
        Thread thread2 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("[Thread-2] Захватил lock1");
                sleep(100);
                synchronized (lock2) {
                    System.out.println("[Thread-2] Захватил lock2 - OK!");
                }
            }
        }, "Thread-2");
        
        thread1.start();
        thread2.start();
        
        try {
            thread1.join();
            thread2.join();
            System.out.println("\n✓ Deadlock избежан! Оба потока завершены.");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    private static void sleep(long ms) {
        try { Thread.sleep(ms); } 
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

4. Решение 2: Timeout при блокировке

Используйте ReentrantLock с tryLock(timeout):

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

public class DeadlockFixed_Timeout {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();
    
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            acquireLocksWithTimeout(lock1, lock2, "Thread-1");
        });
        
        Thread thread2 = new Thread(() -> {
            acquireLocksWithTimeout(lock2, lock1, "Thread-2");
        });
        
        thread1.start();
        thread2.start();
        
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    private static void acquireLocksWithTimeout(ReentrantLock first, ReentrantLock second, String name) {
        while (true) {
            // Пытаемся захватить первый лок с таймаутом
            if (first.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    System.out.println("[" + name + "] Захватил первый лок");
                    sleep(50);
                    
                    // Пытаемся захватить второй лок с таймаутом
                    if (second.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("[" + name + "] Захватил оба лока - OK!");
                            return;  // Успех!
                        } finally {
                            second.unlock();
                        }
                    } else {
                        System.out.println("[" + name + "] Не смог захватить второй лок, отпускаю первый и повторяю");
                    }
                } finally {
                    first.unlock();
                }
            }
            sleep(10);  // Небольшая задержка перед повтором
        }
    }
    
    private static void sleep(long ms) {
        try { Thread.sleep(ms); } 
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

5. Решение 3: Использование ReadWriteLock

Для сценариев read-heavy:

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

public class DeadlockFixed_ReadWrite {
    private static final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public static void main(String[] args) {
        // Множество потоков могут читать одновременно
        Thread reader1 = new Thread(() -> {
            lock.readLock().lock();
            try {
                System.out.println("[Reader-1] читает данные");
                Thread.sleep(100);
            } finally {
                lock.readLock().unlock();
            }
        });
        
        // Только один поток пишет
        Thread writer = new Thread(() -> {
            lock.writeLock().lock();
            try {
                System.out.println("[Writer] пишет данные");
                Thread.sleep(100);
            } finally {
                lock.writeLock().unlock();
            }
        });
        
        reader1.start();
        writer.start();
        
        try { reader1.join(); writer.join(); } 
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

6. Обнаружение Deadlock

С помощью ThreadMXBean

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

public class DeadlockDetector {
    public static void main(String[] args) {
        // Запустите вашу программу с deadlock
        
        // Периодически проверяем
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleAtFixedRate(() -> {
            ThreadMXBean bean = ManagementFactory.getThreadMXBean();
            long[] deadlockedThreads = bean.findDeadlockedThreads();
            
            if (deadlockedThreads != null && deadlockedThreads.length > 0) {
                System.out.println("\n🔴 DEADLOCK ОБНАРУЖЕН!");
                
                ThreadInfo[] infos = bean.getThreadInfo(deadlockedThreads);
                for (ThreadInfo info : infos) {
                    System.out.println("\nПоток: " + info.getThreadName());
                    System.out.println("Статус: " + info.getThreadState());
                    System.out.println("Ждёт блокировки: " + info.getLockName());
                    
                    StackTraceElement[] trace = info.getStackTrace();
                    for (StackTraceElement element : trace) {
                        System.out.println("  at " + element);
                    }
                }
                
                System.exit(1);
            }
        }, 0, 1, TimeUnit.SECONDS);
    }
}

С помощью jstack (в терминале)

# Найти PID процесса Java
jps

# Дамп потоков
jstack <PID>

# В выводе ищите:
# "Found one Java-level deadlock"
# и анализируйте блокировки

7. Лучшие практики

1. Минимизируйте область блокировки

// Плохо
synchronized (lock) {
    // много кода
    heavyComputation();
    database.query();
}

// Хорошо
synchronized (lock) {
    // только критическая секция
    sharedData.update();
}

2. Избегайте вложенных блокировок

// Опасно — риск deadlock
synchronized (lock1) {
    synchronized (lock2) {
        // код
    }
}

// Безопаснее
synchronized (combinedLock) {  // один лок вместо двух
    // код
}

3. Используйте высокоуровневые структуры

// Вместо synchronized используйте:
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

4. Предпочитайте immutable объекты

// Если не нужна синхронизация — не используйте!
final String immutable = "value";
List<String> unmodifiable = Collections.unmodifiableList(list);

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