Deadlock: воспроизведение и решение
Условие
- Напишите код, который гарантированно создаёт deadlock
- Объясните, почему возникает deadlock
- Предложите способы избежать deadlock
Пример ситуации
Два потока пытаются захватить два ресурса (lock1 и lock2), но в разном порядке:
- Поток 1: lock1 → lock2
- Поток 2: lock2 → lock1
Требования
- Продемонстрируйте deadlock
- Используйте synchronized или ReentrantLock
- Покажите решение с упорядочиванием блокировок
- Объясните, как обнаружить deadlock (jstack, ThreadMXBean)
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Deadlock: воспроизведение и решение
Deadlock — это ситуация, когда два или более потока ждут друг друга бесконечно, так как каждый удерживает ресурс, который нужен другому. Это один из самых коварных багов в многопоточных приложениях — он может не проявляться месяцами, а затем неожиданно заморозить систему.
1. Воспроизведение Deadlock
Классический пример с двумя потоками
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// Поток 1: захватывает lock1, потом пытается захватить lock2
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("[Thread-1] Захватил lock1");
sleep(100); // Даём возможность thread2 захватить lock2
System.out.println("[Thread-1] Пытаюсь захватить lock2...");
synchronized (lock2) {
System.out.println("[Thread-1] Захватил lock2");
}
}
}, "Thread-1");
// Поток 2: захватывает lock2, потом пытается захватить lock1
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("[Thread-2] Захватил lock2");
sleep(100); // Даём возможность thread1 захватить lock1
System.out.println("[Thread-2] Пытаюсь захватить lock1...");
synchronized (lock1) {
System.out.println("[Thread-2] Захватил lock1");
}
}
}, "Thread-2");
thread1.start();
thread2.start();
// Ждём завершения (оно никогда не произойдёт!)
try {
thread1.join(2000); // Timeout 2 сек
thread2.join(2000);
if (thread1.isAlive() || thread2.isAlive()) {
System.out.println("\n⚠ DEADLOCK ОБНАРУЖЕН! Потоки зависли.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Вывод:
[Thread-1] Захватил lock1
[Thread-2] Захватил lock2
[Thread-1] Пытаюсь захватить lock2...
[Thread-2] Пытаюсь захватить lock1...
⚠ DEADLOCK ОБНАРУЖЕН! Потоки зависли.
2. Почему возникает deadlock?
Четыре необходимых условия:
- Взаимное исключение — ресурс может использовать только один поток
- Удержание и ожидание — поток удерживает ресурс и ждёт другого
- Отсутствие вытеснения — поток не может забрать ресурс у другого
- Циклическое ожидание — круговая цепочка потоков
В нашем примере:
Поток 1 → [lock1] → ждёт [lock2]
↑
Поток 2 → [lock2] → ждёт [lock1]
↑
Циклическое ожидание! Deadlock.
3. Решение 1: Упорядочивание блокировок
Главный принцип: все потоки должны захватывать ресурсы в одинаковом порядке.
public class DeadlockFixed_Ordering {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// Поток 1: lock1 → lock2 (порядок A)
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("[Thread-1] Захватил lock1");
sleep(100);
synchronized (lock2) {
System.out.println("[Thread-1] Захватил lock2 - OK!");
}
}
}, "Thread-1");
// Поток 2: ТОЖЕ lock1 → lock2 (порядок A)
// ВМЕСТО lock2 → lock1!
Thread thread2 = new Thread(() -> {
synchronized (lock1) {
System.out.println("[Thread-2] Захватил lock1");
sleep(100);
synchronized (lock2) {
System.out.println("[Thread-2] Захватил lock2 - OK!");
}
}
}, "Thread-2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("\n✓ Deadlock избежан! Оба потока завершены.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void sleep(long ms) {
try { Thread.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
4. Решение 2: Timeout при блокировке
Используйте ReentrantLock с tryLock(timeout):
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class DeadlockFixed_Timeout {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
acquireLocksWithTimeout(lock1, lock2, "Thread-1");
});
Thread thread2 = new Thread(() -> {
acquireLocksWithTimeout(lock2, lock1, "Thread-2");
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void acquireLocksWithTimeout(ReentrantLock first, ReentrantLock second, String name) {
while (true) {
// Пытаемся захватить первый лок с таймаутом
if (first.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println("[" + name + "] Захватил первый лок");
sleep(50);
// Пытаемся захватить второй лок с таймаутом
if (second.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println("[" + name + "] Захватил оба лока - OK!");
return; // Успех!
} finally {
second.unlock();
}
} else {
System.out.println("[" + name + "] Не смог захватить второй лок, отпускаю первый и повторяю");
}
} finally {
first.unlock();
}
}
sleep(10); // Небольшая задержка перед повтором
}
}
private static void sleep(long ms) {
try { Thread.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
5. Решение 3: Использование ReadWriteLock
Для сценариев read-heavy:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class DeadlockFixed_ReadWrite {
private static final ReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
// Множество потоков могут читать одновременно
Thread reader1 = new Thread(() -> {
lock.readLock().lock();
try {
System.out.println("[Reader-1] читает данные");
Thread.sleep(100);
} finally {
lock.readLock().unlock();
}
});
// Только один поток пишет
Thread writer = new Thread(() -> {
lock.writeLock().lock();
try {
System.out.println("[Writer] пишет данные");
Thread.sleep(100);
} finally {
lock.writeLock().unlock();
}
});
reader1.start();
writer.start();
try { reader1.join(); writer.join(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
6. Обнаружение Deadlock
С помощью ThreadMXBean
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
public class DeadlockDetector {
public static void main(String[] args) {
// Запустите вашу программу с deadlock
// Периодически проверяем
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = bean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
System.out.println("\n🔴 DEADLOCK ОБНАРУЖЕН!");
ThreadInfo[] infos = bean.getThreadInfo(deadlockedThreads);
for (ThreadInfo info : infos) {
System.out.println("\nПоток: " + info.getThreadName());
System.out.println("Статус: " + info.getThreadState());
System.out.println("Ждёт блокировки: " + info.getLockName());
StackTraceElement[] trace = info.getStackTrace();
for (StackTraceElement element : trace) {
System.out.println(" at " + element);
}
}
System.exit(1);
}
}, 0, 1, TimeUnit.SECONDS);
}
}
С помощью jstack (в терминале)
# Найти PID процесса Java
jps
# Дамп потоков
jstack <PID>
# В выводе ищите:
# "Found one Java-level deadlock"
# и анализируйте блокировки
7. Лучшие практики
1. Минимизируйте область блокировки
// Плохо
synchronized (lock) {
// много кода
heavyComputation();
database.query();
}
// Хорошо
synchronized (lock) {
// только критическая секция
sharedData.update();
}
2. Избегайте вложенных блокировок
// Опасно — риск deadlock
synchronized (lock1) {
synchronized (lock2) {
// код
}
}
// Безопаснее
synchronized (combinedLock) { // один лок вместо двух
// код
}
3. Используйте высокоуровневые структуры
// Вместо synchronized используйте:
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
4. Предпочитайте immutable объекты
// Если не нужна синхронизация — не используйте!
final String immutable = "value";
List<String> unmodifiable = Collections.unmodifiableList(list);
Deadlock — это серьёзная проблема, требующая тщательного проектирования многопоточного кода. Лучший способ избежать его — максимально упростить синхронизацию или использовать готовые потокобезопасные структуры данных.