← Назад к вопросам
Как будешь организовывать код, чтобы не возникал 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'ов:
- ✅ Минимизируй shared mutable state — это главное
- ✅ Если нужны lock'и — берись в одинаковом порядке
- ✅ Используй ReentrantLock с timeout вместо synchronized
- ✅ Используй immutable объекты где возможно
- ✅ Используй высокоуровневые конструкции (ConcurrentHashMap, Reactive Streams)
- ✅ Мониторь потоки (jstack, ThreadMXBean)
- ✅ Пиши unit тесты для многопоточного кода
Золотое правило: лучший lock — это отсутствие lock'а. Если можешь избежать синхронизации — избегай!