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

Как решишь проблему Deadlock при использовании вложенных синхронизованных блоков

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

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

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

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

Решение проблемы Deadlock при вложенных синхронизованных блоках

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

Как возникает дедлок?

// ПРОБЛЕМА: Классический сценарий дедлока
public class Account {
    private double balance = 0;
}

public class Bank {
    private Account accountA = new Account();
    private Account accountB = new Account();
    
    // Поток 1: переводит с A на B
    public void transferAtoB() {
        synchronized(accountA) {
            System.out.println("Поток 1: заблокировал счёт A");
            try { Thread.sleep(100); } catch (Exception e) {}
            
            synchronized(accountB) {  // Ждёт блокировку B
                System.out.println("Поток 1: заблокировал счёт B");
            }
        }
    }
    
    // Поток 2: переводит с B на A
    public void transferBtoA() {
        synchronized(accountB) {
            System.out.println("Поток 2: заблокировал счёт B");
            try { Thread.sleep(100); } catch (Exception e) {}
            
            synchronized(accountA) {  // Ждёт блокировку A
                System.out.println("Поток 2: заблокировал счёт A");
            }
        }
    }
}

Здесь может возникнуть дедлок:

  • Поток 1 блокирует accountA и ждёт accountB
  • Поток 2 блокирует accountB и ждёт accountA
  • Оба потока висят бесконечно

Решение 1: Упорядочение блокировок (Lock Ordering)

Самый надёжный способ — всегда захватывать блокировки в одном и том же порядке:

public class SafeBank {
    private Account accountA = new Account();
    private Account accountB = new Account();
    
    // Решение: всегда блокируем в порядке 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 при блокировке (Lock with Timeout)

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

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

public class SafeBankWithTimeout {
    private Account accountA = new Account();
    private Account accountB = new Account();
    private ReentrantLock lockA = new ReentrantLock();
    private ReentrantLock lockB = new ReentrantLock();
    
    public boolean transfer(double amount) throws InterruptedException {
        // Пытаемся получить блокировку с таймаутом 1 секунда
        if (!lockA.tryLock(1, TimeUnit.SECONDS)) {
            System.out.println("Не смогли заблокировать счёт A, отменяем операцию");
            return false;
        }
        
        try {
            if (!lockB.tryLock(1, TimeUnit.SECONDS)) {
                System.out.println("Не смогли заблокировать счёт B, отменяем операцию");
                return false;
            }
            
            try {
                // Выполняем перевод
                accountA.balance -= amount;
                accountB.balance += amount;
                return true;
            } finally {
                lockB.unlock();
            }
        } finally {
            lockA.unlock();
        }
    }
}

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

Для различных операций чтения и записи:

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

public class SafeAccountWithReadWriteLock {
    private double balance = 0;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    // Несколько потоков могут одновременно читать баланс
    public double getBalance() {
        lock.readLock().lock();
        try {
            return balance;
        } finally {
            lock.readLock().unlock();
        }
    }
    
    // Только один поток может записывать
    public void deposit(double amount) {
        lock.writeLock().lock();
        try {
            balance += amount;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Решение 4: Атомарные операции (Atomic Classes)

Для простых значений используй AtomicLong или AtomicReference:

import java.util.concurrent.atomic.AtomicLong;

public class AtomicAccount {
    private AtomicLong balance = new AtomicLong(0);
    
    public void deposit(long amount) {
        balance.addAndGet(amount);
    }
    
    public long getBalance() {
        return balance.get();
    }
    
    // Без синхронизации! Дедлок невозможен.
}

Решение 5: Использование ConcurrentHashMap и других thread-safe структур

import java.util.concurrent.ConcurrentHashMap;

public class SafeBankWithConcurrentMap {
    private ConcurrentHashMap<String, Account> accounts = new ConcurrentHashMap<>();
    
    // ConcurrentHashMap использует fine-grained locking
    // Дедлоки между разными ключами невозможны
    public void transfer(String fromId, String toId, double amount) {
        Account from = accounts.get(fromId);
        Account to = accounts.get(toId);
        
        // Используем computeIfPresent для атомарных операций
        accounts.computeIfPresent(fromId, (id, acc) -> {
            acc.balance -= amount;
            return acc;
        });
        
        accounts.computeIfPresent(toId, (id, acc) -> {
            acc.balance += amount;
            return acc;
        });
    }
}

Лучшие практики избежания дедлоков

1. Минимизируй область синхронизации

// ДО: Слишком много кода в synchronized
synchronized(lock) {
    // 100 строк кода
    // Высокий риск дедлока
}

// ПОСЛЕ: Только критическая секция
synchronized(lock) {
    sharedResource.update();
}

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

// ДО: Вложенные synchronized
synchronized(lockA) {
    synchronized(lockB) {
        // Риск дедлока
    }
}

// ПОСЛЕ: Один объект для синхронизации
synchronized(combinedLock) {
    // Нет дедлока
}

3. Используй правило "не захватывай новые блокировки, если уже держишь"

// ДО: Захватываем новую блокировку внутри
public synchronized void method1() {
    method2(); // method2 тоже synchronized!
}

public synchronized void method2() {
    // Может привести к дедлоку
}

// ПОСЛЕ: Если необходимо, разделяй логику
public void method1() {
    synchronized(lock) {
        doWork1();
    }
}

Диагностика дедлоков

Если дедлок уже произошёл, можешь использовать инструменты:

# 1. Получить потокdump
jstack <PID>

# 2. Анализ с помощью JConsole
jconsole

# 3. Использовать ThreadMXBean для программной проверки
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = bean.findDeadlockedThreads();
if (deadlockedThreads != null) {
    System.out.println("Обнаружен дедлок!");
}

Рекомендация

Для новых проектов избегай synchronized блоков. Используй:

  • java.util.concurrent.* — безопасные коллекции (ConcurrentHashMap, CopyOnWriteArrayList)
  • ReentrantLock — явное управление блокировками
  • Atomic* — для простых значений
  • CountDownLatch, CyclicBarrier — для синхронизации потоков

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