Что такое гарантированный дедлок?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Гарантированный дедлок в многопоточности
Гарантированный дедлок (deadlock) — это ситуация в многопоточной программе, когда два или более потока остаются заблокированными навечно, ожидая друг друга в циклической зависимости. Ни один из потоков не может продолжить свою работу, и приложение зависает.
Дедлок является одной из самых опасных проблем многопоточного программирования, так как может проявляться нечасто и в непредсказуемых ситуациях, что затрудняет диагностику и отладку.
Условия возникновения дедлока
Для возникновения дедлока необходимо выполнение всех четырёх условий одновременно:
1. Взаимное исключение (Mutual Exclusion) — ресурс может быть использован только одним потоком одновременно:
private final Object lock = new Object();
synchronized(lock) {
// Только один поток может быть здесь одновременно
}
2. Удержание и ожидание (Hold and Wait) — поток удерживает один ресурс и ждёт другого:
public void transferMoney(Account from, Account to, BigDecimal amount) {
synchronized(from) { // Удерживаем первый ресурс
// Пока ждём второй ресурс
synchronized(to) {
// Выполняем перевод
}
}
}
3. Отсутствие вытеснения (No Preemption) — ресурс нельзя забрать у потока — он может его отпустить только добровольно:
// Когда поток захватил lock, никто не может его отнять
private final Lock lock = new ReentrantLock();
lock.lock(); // Захватили
// Если поток зависнет, lock остаётся захвачен
lock.unlock(); // Только этот поток может отпустить
4. Циклическое ожидание (Circular Wait) — создаётся цикл зависимостей между потоками:
// Поток 1: ждёт B, удерживает A
// Поток 2: ждёт A, удерживает B
// Это образует цикл: A -> B -> A
Классический пример дедлока
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void thread1Method() {
synchronized(lock1) {
System.out.println("Поток 1: захватил lock1");
try {
Thread.sleep(100); // Имитация работы
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Поток 1: ждёт lock2");
synchronized(lock2) {
System.out.println("Поток 1: захватил lock2");
}
}
}
public static void thread2Method() {
synchronized(lock2) {
System.out.println("Поток 2: захватил lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Поток 2: ждёт lock1");
synchronized(lock1) {
System.out.println("Поток 2: захватил lock1");
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(DeadlockExample::thread1Method);
Thread t2 = new Thread(DeadlockExample::thread2Method);
t1.start();
t2.start();
// Программа зависнет:
// Поток 1 захватил lock1 и ждёт lock2
// Поток 2 захватил lock2 и ждёт lock1
}
}
Пример дедлока с banco-переводами
Это классическая задача, где дедлоки особенно вероятны:
public class BankAccount {
private String accountId;
private BigDecimal balance;
public synchronized void transfer(BankAccount to, BigDecimal amount) {
if (this.balance.compareTo(amount) >= 0) {
this.balance = this.balance.subtract(amount);
to.deposit(amount);
}
}
public synchronized void deposit(BigDecimal amount) {
this.balance = this.balance.add(amount);
}
}
// Использование:
BankAccount acc1 = new BankAccount("1", new BigDecimal("1000"));
BankAccount acc2 = new BankAccount("2", new BigDecimal("1000"));
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
acc1.transfer(acc2, new BigDecimal("10"));
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
acc2.transfer(acc1, new BigDecimal("10"));
}
});
t1.start();
t2.start();
// Высокая вероятность дедлока!
Способы предотвращения дедлока
1. Избегание циклических зависимостей — упорядоченное захватывание ресурсов:
public class OrderedLockTransfer {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void transfer() {
// ВСЕГДА захватываем в одном и том же порядке
synchronized(lock1) {
synchronized(lock2) {
System.out.println("Перевод выполнен");
}
}
}
}
2. Использование timeout с Lock:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TimeoutLockExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void safeTransfer() throws InterruptedException {
lock1.lock();
try {
// Пытаемся захватить второй lock с timeout
if (lock2.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println("Оба lock захвачены");
} finally {
lock2.unlock();
}
} else {
System.out.println("Не удалось захватить lock2, отпускаем lock1");
}
} finally {
lock1.unlock();
}
}
}
3. Использование CyclicBarrier или других синхронизирующих структур:
import java.util.concurrent.CyclicBarrier;
public class BarrierSynchronization {
private final CyclicBarrier barrier = new CyclicBarrier(2);
public void synchronizedWork() throws Exception {
// Оба потока встречаются здесь, нет циклического ожидания
barrier.await();
}
}
4. Использование высокоуровневых структур (меньше вероятность ошибок):
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
// AtomicInteger использует внутренние механизмы, нет дедлоков
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // Безопасно и без дедлоков
}
}
5. Использование tryLock() с проверкой:
public boolean transferWithRetry(Account from, Account to, BigDecimal amount)
throws InterruptedException {
while (true) {
if (from.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (to.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
try {
from.withdraw(amount);
to.deposit(amount);
return true;
} finally {
to.getLock().unlock();
}
}
} finally {
from.getLock().unlock();
}
}
// Если не удалось, пробуем ещё раз
Thread.sleep(10);
}
}
Обнаружение дедлока
В Java можно обнаружить дедлоки через JMX или дампы потоков:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
public class DeadlockDetector {
public static void checkForDeadlocks() {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = mxBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
System.out.println("ОБНАРУЖЕНЫ ДЕДЛОКИ! Потоков: " + deadlockedThreads.length);
var threadInfo = mxBean.getThreadInfo(deadlockedThreads);
for (var info : threadInfo) {
System.out.println("Поток: " + info.getThreadName());
System.out.println("Ожидает: " + info.getLockName());
}
}
}
}
Предотвращение дедлоков требует осторожного проектирования и понимания всех возможных сценариев взаимодействия потоков в приложении.