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

Какие знаешь способы избежать Dead Lock?

2.2 Middle🔥 151 комментариев
#Основы Java

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

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

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

Способы избежать Deadlock в многопоточном программировании

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

Что такое Deadlock

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

Object lock1 = new Object();
Object lock2 = new Object();

// Thread 1
Thread thread1 = new Thread(() -> {
    synchronized (lock1) {
        System.out.println("T1: Got lock1");
        try { Thread.sleep(100); } catch (Exception e) {}
        synchronized (lock2) {  // Ждет lock2
            System.out.println("T1: Got lock2");
        }
    }
});

// Thread 2
Thread thread2 = new Thread(() -> {
    synchronized (lock2) {
        System.out.println("T2: Got lock2");
        try { Thread.sleep(100); } catch (Exception e) {}
        synchronized (lock1) {  // Ждет lock1
            System.out.println("T2: Got lock1");
        }
    }
});

thread1.start();
thread2.start();
// DEADLOCK! Они ждут друг друга вечно

Четыре условия для Deadlock

Для возникновения deadlock необходимы ВСЕ четыре условия одновременно:

  1. Mutual Exclusion (Взаимное исключение): Ресурс может использоваться только одним потоком
  2. Hold and Wait (Держи и жди): Поток держит один ресурс и ждет другой
  3. No Preemption (Нет вытеснения): Ресурс нельзя отобрать силой
  4. Circular Wait (Циклическое ожидание): Есть циклическая цепочка потоков, ждущих друг друга

Способы избежать Deadlock

1. Избежать циклического ожидания (Circular Wait)

Способ: Всегда захватывай локи в ОДНОМ порядке.

// ПЛОХО: разные потоки в разном порядке
Thread t1 = new Thread(() -> {
    synchronized (lock1) {
        synchronized (lock2) {}
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lock2) {      // Другой порядок!
        synchronized (lock1) {}
    }
});

// ХОРОШО: всегда в одном порядке
Thread t1 = new Thread(() -> {
    synchronized (lock1) {
        synchronized (lock2) {}
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lock1) {      // Такой же порядок
        synchronized (lock2) {}
    }
});

На практике:

public class Account {
    private long id;
    private double balance;
    
    private static final Object globalLock = new Object();
    
    // Всегда берем локи по возрастанию ID
    public void transfer(Account from, Account to, double amount) {
        Account first = from.id < to.id ? from : to;
        Account second = from.id < to.id ? to : from;
        
        synchronized (first) {
            synchronized (second) {
                from.balance -= amount;
                to.balance += amount;
            }
        }
    }
}

2. Использовать timeout при захвате локов

ReentrantLock с timeout:

ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();

public void safeMethod() throws InterruptedException {
    if (!lock1.tryLock(1, TimeUnit.SECONDS)) {
        System.out.println("Не смогли захватить lock1");
        return;  // Отступаем, вместо блокировки
    }
    
    try {
        if (!lock2.tryLock(1, TimeUnit.SECONDS)) {
            System.out.println("Не смогли захватить lock2");
            return;  // Отступаем
        }
        
        try {
            // Логика с двумя локами
        } finally {
            lock2.unlock();
        }
    } finally {
        lock1.unlock();
    }
}

3. Избежать вложенных синхронизаций

ПЛОХО: вложенные synchronized

public synchronized void method1() {
    // долгие операции
    method2();  // Вложенный synchronized
}

public synchronized void method2() {
    // ещё операции
}

ХОРОШО: разделить логику

public void method1() {
    synchronized (this) {
        // долгие операции
    }
    method2();  // Без вложенности
}

public void method2() {
    synchronized (this) {
        // операции
    }
}

4. Использовать immutable объекты

Immutable объекты не требуют синхронизации:

public final class ImmutablePoint {
    private final int x;
    private final int y;
    
    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() { return x; }
    public int getY() { return y; }
    
    // Нет setters!
}

// Использование безопасно для многопоточности
ImmutablePoint point = new ImmutablePoint(1, 2);
// Несколько потоков могут читать point без deadlock

5. Использовать ConcurrentHashMap и другие thread-safe коллекции

ПЛОХО: вложенные синхронизации на коллекциях

Map<String, Integer> map = new HashMap<>();

public void badMethod() {
    synchronized (map) {
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            synchronized (entry) {  // Риск deadlock
                // обработка
            }
        }
    }
}

ХОРОШО: использовать ConcurrentHashMap

Map<String, Integer> map = new ConcurrentHashMap<>();

public void goodMethod() {
    // Не нужна явная синхронизация!
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        // ConcurrentHashMap сама позаботится о безопасности
    }
}

6. Использовать ReadWriteLock

ReadWriteLock для read-heavy операций:

public class Cache<K, V> {
    private final Map<K, V> cache = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public V get(K key) {
        lock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    public void put(K key, V value) {
        lock.writeLock().lock();
        try {
            cache.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

// Несколько потоков могут одновременно читать
// Только при write захватывается exclusive lock

7. Использовать Semaphore

Semaphore для ограничения доступа:

public class ResourcePool {
    private final Semaphore semaphore = new Semaphore(3);  // Макс 3 потока
    private final List<Resource> resources = new ArrayList<>();
    
    public Resource acquire() throws InterruptedException {
        semaphore.acquire();
        synchronized (resources) {
            return resources.remove(0);
        }
    }
    
    public void release(Resource resource) {
        synchronized (resources) {
            resources.add(resource);
        }
        semaphore.release();
    }
}

// Предотвращает ситуации, где все потоки ждут друг друга

8. Использовать CountDownLatch и Phaser

CountDownLatch для синхронизации:

public class Task {
    private final CountDownLatch latch = new CountDownLatch(3);
    
    public void execute() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            // работа
            latch.countDown();
        });
        
        Thread t2 = new Thread(() -> {
            // работа
            latch.countDown();
        });
        
        t1.start();
        t2.start();
        
        latch.await();  // Ждем, пока все потоки не закончат
        System.out.println("Все потоки завершили работу");
    }
}

// Более безопасный способ синхронизации, чем вложенные synchronized

9. Использовать Executor Service

Executor Service управляет потоками безопасно:

public class SafeThreading {
    private final ExecutorService executor = Executors.newFixedThreadPool(4);
    
    public void execute() {
        executor.submit(() -> {
            // Task 1
        });
        
        executor.submit(() -> {
            // Task 2
        });
        
        try {
            executor.shutdown();
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}

// Executor сам управляет очередью и потоками

10. Отладка Deadlock

Как найти deadlock в code:

public void detectDeadlock() {
    ThreadMXBean bean = ManagementFactory.getThreadMXBean();
    long[] ids = bean.findMonitorDeadlockedThreads();
    
    if (ids != null && ids.length > 0) {
        System.out.println("DEADLOCK ОБНАРУЖЕН!");
        ThreadInfo[] infos = bean.getThreadInfo(ids, Integer.MAX_VALUE);
        
        for (ThreadInfo info : infos) {
            System.out.println("Поток: " + info.getThreadName());
            System.out.println("Ждет: " + info.getLockName());
        }
    }
}

Использовать jstack для анализа:

# В другом терминале
jstack <pid> | grep -A 20 "Found one Java-level deadlock"

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

  • Избегай synchronized: используй ReentrantLock с timeout
  • Один порядок локов: всегда захватывай в одном порядке
  • Минимизируй critical section: держи лок как можно меньше
  • Используй high-level API: ConcurrentHashMap, Executor Service
  • Immutability первая: неизменяемые объекты безопаснее
  • Тестируй под нагрузкой: deadlock проявляется при load
  • Документируй: если используешь multiple locks, документируй порядок

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

Какие знаешь способы избежать Dead Lock? | PrepBro