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

Как будешь организовывать код, чтобы не возникал deadlock

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

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

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

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

Ответ

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

Что такое Deadlock

Поток A              Поток B
   ↓                    ↓
Берёт Lock1          Берёт Lock2
   ↓                    ↓
Ожидает Lock2        Ожидает Lock1
   ↓                    ↓
  DEADLOCK! (вечное ожидание)

Пример классического deadlock:

// ❌ DEADLOCK!
public class Account {
    private BigDecimal balance;
    
    public synchronized void transferTo(Account other, BigDecimal amount) {
        // Поток A: берёт this.lock, ждёт other.lock
        this.balance -= amount;
        other.withdraw(amount);  // Вложенный synchronized на other
    }
    
    private synchronized void withdraw(BigDecimal amount) {
        this.balance += amount;
    }
}

// Использование
Account a = new Account(1000);
Account b = new Account(500);

// Поток 1: a.transferTo(b, 100) — берёт a.lock, ждёт b.lock
Thread t1 = new Thread(() -> a.transferTo(b, 100));

// Поток 2: b.transferTo(a, 200) — берёт b.lock, ждёт a.lock
Thread t2 = new Thread(() -> b.transferTo(a, 200));

t1.start();
t2.start();
// Оба потока зависнут в deadlock!

Условия для Deadlock (все 4 должны быть верны)

1. Mutual Exclusion — ресурсы не могут быть одновременно использованы
2. Hold and Wait — потоки держат ресурсы и ждут других
3. No Preemption — нельзя отобрать ресурс у потока
4. Circular Wait — циклическая цепь ожидания ресурсов

Стратегия 1: Избегать вложенных Synchronized

// ✅ Хорошо
public class Account {
    private BigDecimal balance;
    private final Object balanceLock = new Object();
    
    public void transferTo(Account other, BigDecimal amount) {
        // Берём локи в ОДИНАКОВОМ порядке
        Account first, second;
        if (System.identityHashCode(this) < System.identityHashCode(other)) {
            first = this;
            second = other;
        } else {
            first = other;
            second = this;
        }
        
        synchronized (first.balanceLock) {
            synchronized (second.balanceLock) {
                this.balance -= amount;
                other.balance += amount;
            }
        }
    }
}

Ключевая идея: всегда берись за locki в одинаковом порядке (например, по identityHashCode).

Стратегия 2: Использовать TimeOut

public class TransferService {
    public boolean transferWithTimeout(Account a, Account b, BigDecimal amount) {
        ReentrantLock lockA = a.getLock();
        ReentrantLock lockB = b.getLock();
        
        try {
            // Берём первый лок с timeout
            if (!lockA.tryLock(1, TimeUnit.SECONDS)) {
                return false;  // Не смогли получить лок
            }
            
            try {
                // Берём второй лок с timeout
                if (!lockB.tryLock(1, TimeUnit.SECONDS)) {
                    return false;
                }
                
                try {
                    // Выполняем операцию
                    a.withdraw(amount);
                    b.deposit(amount);
                    return true;
                } finally {
                    lockB.unlock();
                }
            } finally {
                lockA.unlock();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
}

Стратегия 3: Использовать ReentrantLock вместо Synchronized

public class Account {
    private BigDecimal balance;
    private final ReentrantLock lock = new ReentrantLock();
    
    public void transferTo(Account other, BigDecimal amount) throws InterruptedException {
        // ReentrantLock имеет tryLock с timeout
        if (!lock.tryLock(5, TimeUnit.SECONDS)) {
            throw new TimeoutException("Could not acquire lock");
        }
        
        try {
            if (!other.getLock().tryLock(5, TimeUnit.SECONDS)) {
                throw new TimeoutException("Could not acquire lock on other");
            }
            
            try {
                this.balance -= amount;
                other.balance += amount;
            } finally {
                other.getLock().unlock();
            }
        } finally {
            lock.unlock();
        }
    }
    
    public ReentrantLock getLock() {
        return lock;
    }
}

Стратегия 4: Использовать Immutable объекты

// ✅ Отличное решение — immutable
public final class Account {
    private final String id;
    private final BigDecimal balance;  // final — не меняется
    
    public Account(String id, BigDecimal balance) {
        this.id = id;
        this.balance = balance;
    }
    
    // Вместо mutating — создаём новый объект
    public Account transferTo(Account other, BigDecimal amount) {
        return new Account(this.id, this.balance.subtract(amount));
    }
    
    public Account receiveFrom(Account other, BigDecimal amount) {
        return new Account(this.id, this.balance.add(amount));
    }
}

// Использование
Account a = new Account("A", BigDecimal.valueOf(1000));
Account b = new Account("B", BigDecimal.valueOf(500));

Account newA = a.transferTo(b, BigDecimal.valueOf(100));  // a не изменяется
Account newB = b.receiveFrom(a, BigDecimal.valueOf(100));

// Нет deadlock, потому что нет изменяемого состояния!

Стратегия 5: Использовать ConcurrentHashMap или Collections.synchronizedMap

// ✅ ConcurrentHashMap использует segment locks (меньше contention)
public class UserCache {
    private final ConcurrentHashMap<Long, User> cache = new ConcurrentHashMap<>();
    
    public void updateUser(Long id, User user) {
        cache.put(id, user);  // Безопасно, нет deadlock риска
    }
    
    public User getUser(Long id) {
        return cache.get(id);
    }
}

// ❌ Collections.synchronizedMap — весь map заблокирован
Map<Long, User> cache = Collections.synchronizedMap(new HashMap<>());

Стратегия 6: Использовать ReadWriteLock (если mostly read)

public class Cache<K, V> {
    private final Map<K, V> data = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public V get(K key) {
        lock.readLock().lock();  // Много потоков могут читать
        try {
            return data.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    public void put(K key, V value) {
        lock.writeLock().lock();  // Только один поток может писать
        try {
            data.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Стратегия 7: Использовать Actor Model (Akka)

// Вместо общих lock'ов, каждый Actor имеет свой mailbox
public class AccountActor extends AbstractActor {
    private BigDecimal balance;
    
    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(Deposit.class, msg -> {
                this.balance = balance.add(msg.amount);
                sender().tell(new Success(), self());
            })
            .match(Withdraw.class, msg -> {
                if (balance.compareTo(msg.amount) >= 0) {
                    this.balance = balance.subtract(msg.amount);
                    sender().tell(new Success(), self());
                } else {
                    sender().tell(new Failure("Insufficient funds"), self());
                }
            })
            .build();
    }
}

// Использование
ActorRef accountA = system.actorOf(AccountActor.class);
ActorRef accountB = system.actorOf(AccountActor.class);

// Сообщения обрабатываются последовательно в Actor
// Нет deadlock, потому что нет shared state!
accountA.tell(new Withdraw(100), ActorRef.noSender());
accountB.tell(new Deposit(100), ActorRef.noSender());

Стратегия 8: Использовать Reactive Streams (Project Reactor)

public class ReactiveAccountService {
    private final Map<Long, BigDecimal> balances = new ConcurrentHashMap<>();
    
    public Mono<Void> transferAsync(Long fromId, Long toId, BigDecimal amount) {
        return Mono
            .fromCallable(() -> {
                BigDecimal balance = balances.get(fromId);
                if (balance.compareTo(amount) >= 0) {
                    balances.put(fromId, balance.subtract(amount));
                    balances.compute(toId, (k, v) -> v.add(amount));
                    return true;
                }
                return false;
            })
            .filter(success -> success)
            .switchIfEmpty(Mono.error(new IllegalArgumentException("Transfer failed")))
            .then();  // Возвращает Mono<Void>
    }
}

// Использование
Mono.zip(
    service.transferAsync(1L, 2L, amount),
    service.transferAsync(3L, 4L, amount)
).block();  // Обе операции могут выполняться параллельно без deadlock

Мой порядок приоритета

1️⃣ ПЕРВОЕ МЕСТО: Избежать shared mutable state
   → Используй Immutable объекты
   → Используй ConcurrentHashMap вместо synchronized
   → Используй Reactive Streams

2️⃣ ВТОРОЕ МЕСТО: Если shared state необходим
   → Всегда берись за locki в одинаковом порядке
   → Используй ReentrantLock с tryLock(timeout)
   → Минимизируй время под lock

3️⃣ ТРЕТЬЕ МЕСТО: Не используй вложенные synchronized
   → Объедини логику в один synchronized блок
   → Или используй ReentrantLock

4️⃣ ЧЕТВЁРТОЕ МЕСТО: Используй Actor Model
   → Для очень сложных систем
   → Каждый Actor обрабатывает сообщения последовательно

Инструменты для поиска Deadlock'ов

// ThreadMXBean — утилита для анализа потоков
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] threadIds = threadMXBean.findMonitorDeadlockedThreads();

if (threadIds != null && threadIds.length > 0) {
    ThreadInfo[] infos = threadMXBean.getThreadInfo(threadIds, true, true);
    for (ThreadInfo info : infos) {
        System.out.println("DEADLOCK DETECTED: " + info.getThreadName());
        System.out.println(info);
    }
}

// jstack — консольная утилита для дампа потоков
// jstack <pid>  # Показывает все потоки и их блокировки

Real-world пример: Spring Boot Service

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Transactional  // Spring управляет transactional lock'ами
    public Order createOrder(CreateOrderRequest request) {
        // 1. БД автоматически управляет row locks в транзакции
        Order order = new Order(request);
        orderRepository.save(order);
        
        // 2. InventoryService может быть вызван — Spring гарантирует
        //    что он не создаст deadlock с правильной конфигурацией
        inventoryService.reserve(order.getItems());
        
        return order;
    }
}

Итог

Чтобы избежать deadlock'ов:

  1. Минимизируй shared mutable state — это главное
  2. Если нужны lock'и — берись в одинаковом порядке
  3. Используй ReentrantLock с timeout вместо synchronized
  4. Используй immutable объекты где возможно
  5. Используй высокоуровневые конструкции (ConcurrentHashMap, Reactive Streams)
  6. Мониторь потоки (jstack, ThreadMXBean)
  7. Пиши unit тесты для многопоточного кода

Золотое правило: лучший lock — это отсутствие lock'а. Если можешь избежать синхронизации — избегай!