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

Напиши код для гарантированного дедлока

3.0 Senior🔥 131 комментариев
#Многопоточность

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

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

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

Гарантированный дедлок в Java

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

Классический дедлок: два потока, два lock-а

import java.util.concurrent.CountDownLatch;

public class DeadlockExample {
    
    static class Account {
        private int balance;
        private int id;
        
        public Account(int id, int balance) {
            this.id = id;
            this.balance = balance;
        }
        
        public synchronized void transfer(Account to, int amount) {
            System.out.println(Thread.currentThread().getName() + 
                " пытается перевести " + amount + " со счёта " + this.id + 
                " на счёт " + to.id);
            
            // Это создаст дедлок!
            synchronized (to) {
                System.out.println(Thread.currentThread().getName() + 
                    " получил оба лока");
                
                this.balance -= amount;
                to.balance += amount;
                
                System.out.println(Thread.currentThread().getName() + 
                    " завершил перевод");
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Account account1 = new Account(1, 1000);
        Account account2 = new Account(2, 2000);
        
        // Лок 1: поток 1 получит account1, потом будет ждать account2
        // Лок 2: поток 2 получит account2, потом будет ждать account1
        // Результат: ДЕДЛОК
        
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                account1.transfer(account2, 100);
            }
        }, "Thread-1");
        
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                account2.transfer(account1, 100);
            }
        }, "Thread-2");
        
        thread1.start();
        thread2.start();
        
        // Ждём 5 секунд — потоки будут зависшими
        Thread.sleep(5000);
        System.out.println("\n⚠️ ДЕДЛОК! Потоки зависли.");
    }
}

Почему это дедлок?

Этап 1:
  Thread-1: synchronized (account1) — получил лок 1 ✓
  Thread-2: synchronized (account2) — получил лок 2 ✓

Этап 2:
  Thread-1: synchronized (account2) — ЖДЁТ лок 2 (занят Thread-2) ⏳
  Thread-2: synchronized (account1) — ЖДЁТ лок 1 (занят Thread-1) ⏳

Вечный цикл ожидания — ДЕДЛОК!

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

Необходимы ВСЕ 4 условия одновременно:

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

Вариант 2: дедлок с ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDeadlock {
    
    static class Resource {
        private ReentrantLock lock = new ReentrantLock();
        private String name;
        
        public Resource(String name) {
            this.name = name;
        }
        
        public void access(Resource other) {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + 
                " захватил лок " + this.name);
            
            try {
                Thread.sleep(100); // Симулируем обработку
                
                other.lock.lock();
                System.out.println(Thread.currentThread().getName() + 
                    " захватил лок " + other.name);
                
                try {
                    System.out.println("Успешно!");
                } finally {
                    other.lock.unlock();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Resource resource1 = new Resource("Resource-1");
        Resource resource2 = new Resource("Resource-2");
        
        Thread thread1 = new Thread(() -> {
            resource1.access(resource2);
        }, "T1");
        
        Thread thread2 = new Thread(() -> {
            resource2.access(resource1);
        }, "T2");
        
        thread1.start();
        thread2.start();
        
        Thread.sleep(3000);
        System.out.println("⚠️ Дедлок с ReentrantLock!");
    }
}

Вариант 3: дедлок с несколькими потоками

public class MultiThreadDeadlock {
    
    static Object lock1 = new Object();
    static Object lock2 = new Object();
    static Object lock3 = new Object();
    
    public static void main(String[] args) {
        // Thread-1: захватит lock1, ждёт lock2
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("T1 захватил lock1");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                
                synchronized (lock2) {
                    System.out.println("T1 захватил lock2");
                }
            }
        });
        
        // Thread-2: захватит lock2, ждёт lock3
        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("T2 захватил lock2");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                
                synchronized (lock3) {
                    System.out.println("T2 захватил lock3");
                }
            }
        });
        
        // Thread-3: захватит lock3, ждёт lock1
        Thread t3 = new Thread(() -> {
            synchronized (lock3) {
                System.out.println("T3 захватил lock3");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                
                synchronized (lock1) {
                    System.out.println("T3 захватил lock1");
                }
            }
        });
        
        t1.start();
        t2.start();
        t3.start();
        
        // Все три потока зависнут в круговом ожидании
    }
}

Вариант 4: дедлок с BlockingQueue

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueDeadlock {
    
    static class Producer implements Runnable {
        private BlockingQueue<String> queue1;
        private BlockingQueue<String> queue2;
        
        public Producer(BlockingQueue<String> queue1, BlockingQueue<String> queue2) {
            this.queue1 = queue1;
            this.queue2 = queue2;
        }
        
        @Override
        public void run() {
            try {
                String item = queue1.take(); // Ждёт элемента из queue1
                System.out.println("P: получил " + item);
                queue2.put(item + "-processed"); // Кладёт в queue2
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue1 = new LinkedBlockingQueue<>(1);
        BlockingQueue<String> queue2 = new LinkedBlockingQueue<>(1);
        
        // Заполняем обе очереди
        queue1.put("item1");
        queue2.put("item2");
        
        // Теперь очереди полные
        // Thread1: попробует взять из queue1 и положить в queue2
        // Но queue2 полная! Ждёт, пока из queue2 возьмут
        Thread t1 = new Thread(new Producer(queue1, queue2), "T1");
        
        // Thread2: попробует взять из queue2 и положить в queue1
        // Но queue1 полная! Ждёт, пока из queue1 возьмут
        Thread t2 = new Thread(new Producer(queue2, queue1), "T2");
        
        t1.start();
        t2.start();
        
        Thread.sleep(2000);
        System.out.println("⚠️ Дедлок с BlockingQueues!");
    }
}

Диагностика дедлока: Thread Dump

Если программа зависла, можно получить информацию о дедлоке:

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

# Получить thread dump
jstack <PID>

# Вывод покажет:
# Found one Java-level deadlock:
# =============================
# "Thread-2":
#   waiting to lock monitor 0x00007f8b8d...
#   (owned by "Thread-1" at 0x00007f8b8d...)

Как избежать дедлока?

1. Всегда захватывать локи в одинаковом порядке

// ❌ Плохо — может быть дедлок
thread1: synchronized(A) { synchronized(B) {} }
thread2: synchronized(B) { synchronized(A) {} }

// ✅ Хорошо — обе потоки берут в порядке A → B
thread1: synchronized(A) { synchronized(B) {} }
thread2: synchronized(A) { synchronized(B) {} }

2. Использовать timeout

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

ReentrantLock lock = new ReentrantLock();

if (lock.tryLock(5, TimeUnit.SECONDS)) {
    try {
        // Работаем с ресурсом
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("Не смогли получить лок за 5 секунд");
}

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

import java.util.concurrent.ConcurrentHashMap;

// Вместо synchronized — используем ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();

4. Deadlock detection в production

public class DeadlockDetector extends Thread {
    @Override
    public void run() {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        
        while (true) {
            long[] deadlockedThreads = bean.findDeadlockedThreads();
            
            if (deadlockedThreads != null && deadlockedThreads.length > 0) {
                System.err.println("ДЕДЛОК ОБНАРУЖЕН!");
                ThreadInfo[] info = bean.getThreadInfo(deadlockedThreads);
                for (ThreadInfo ti : info) {
                    System.err.println(ti);
                }
            }
            
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

Заключение

Дедлок — это серьёзная проблема в многопоточных приложениях. Он возникает, когда два или более потока ждут друг друга в круговой зависимости. Главное правило для избежания дедлока — всегда захватывать локи в одинаковом порядке. Используйте высокоуровневые инструменты (ConcurrentHashMap, CountDownLatch, Semaphore), избегайте вложенной синхронизации, и применяйте timeout при захвате локов.

Напиши код для гарантированного дедлока | PrepBro