Что такое deadlock (взаимная блокировка)? Как его избежать?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Deadlock (взаимная блокировка) в Java
Определение Deadlock
Deadlock — это ситуация, когда два или более потока ждут друг друга бесконечно и ни один из них не может продолжить работу.
Поток 1: ждёт ресурс B (занят Потоком 2)
↓
Поток 2: ждёт ресурс A (занят Потоком 1)
↓
Ни один поток не может двигаться → DEADLOCK
Классический пример Deadlock
public class DeadlockExample {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
// Поток 1
public static void method1() throws InterruptedException {
synchronized (lock1) { // Захватил lock1
System.out.println("Thread 1: захватил lock1");
Thread.sleep(1000); // Имитация работы
synchronized (lock2) { // Ждёт lock2 (занят Потоком 2)
System.out.println("Thread 1: захватил lock2");
}
}
}
// Поток 2
public static void method2() throws InterruptedException {
synchronized (lock2) { // Захватил lock2
System.out.println("Thread 2: захватил lock2");
Thread.sleep(1000); // Имитация работы
synchronized (lock1) { // Ждёт lock1 (занят Потоком 1)
System.out.println("Thread 2: захватил lock1");
}
}
}
public static void main(String[] args) {
new Thread(() -> {
try { method1(); } catch (InterruptedException e) { }
}).start();
new Thread(() -> {
try { method2(); } catch (InterruptedException e) { }
}).start();
// Результат: DEADLOCK!
// Thread 1 ждёт lock2, Thread 2 ждёт lock1
// Программа зависает навечно
}
}
Условия для Deadlock (все 4 должны быть истинны)
1. Mutual Exclusion (Взаимное исключение)
// Ресурс может использоваться только одним потоком
synchronized (lock) {
// Только один поток может быть здесь
}
2. Hold and Wait (Удержание и ожидание)
synchronized (lock1) { // Удерживаем lock1
// ...
synchronized (lock2) { // И ждём lock2
// Deadlock возможен если другой поток делает наоборот
}
}
3. No Preemption (Нет вытеснения)
// Нельзя насильно отобрать ресурс у потока
// Поток должен сам отпустить lock
synchronized (lock) {
// Только сам поток может отпустить lock
}
4. Circular Wait (Циклическое ожидание)
Поток 1: lock1 → lock2 → lock3 → lock1 (цикл!)
Поток 2: lock2 → lock3 → lock1 → lock2 (цикл!)
Как избежать Deadlock
1. Фиксированный порядок захвата ресурсов (BEST PRACTICE)
Всегда захватывайте ресурсы в одном и том же порядке:
public class NoDeadlockExample {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
// Всегда: сначала lock1, потом lock2
public static void method1() throws InterruptedException {
synchronized (lock1) { // ✓ Первый
Thread.sleep(1000);
synchronized (lock2) { // ✓ Второй
System.out.println("Method 1 успешно");
}
}
}
public static void method2() throws InterruptedException {
synchronized (lock1) { // ✓ Первый (тот же порядок!)
Thread.sleep(1000);
synchronized (lock2) { // ✓ Второй (тот же порядок!)
System.out.println("Method 2 успешно");
}
}
}
// Результат: NO DEADLOCK ✓
}
Почему это работает:
Поток 1: захватил lock1 → ждёт lock2
Поток 2: ждёт lock1 (захватит, когда Поток 1 отпустит)
→ потом ждёт lock2
Без цикла, deadlock невозможен!
2. Таймауты (Timeout) вместо вечного ожидания
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class DeadlockWithTimeout {
private ReentrantLock lock1 = new ReentrantLock();
private ReentrantLock lock2 = new ReentrantLock();
public boolean transferMoney() throws InterruptedException {
boolean acquired1 = false, acquired2 = false;
try {
// Пытаемся захватить с таймаутом (2 секунды)
acquired1 = lock1.tryLock(2, TimeUnit.SECONDS);
if (!acquired1) return false; // Не получили lock1
acquired2 = lock2.tryLock(2, TimeUnit.SECONDS);
if (!acquired2) return false; // Не получили lock2
// Оба локка захвачены, выполняем операцию
System.out.println("Транзакция успешна");
return true;
} finally {
// Гарантированно отпускаем ресурсы
if (acquired2) lock2.unlock();
if (acquired1) lock1.unlock();
}
}
}
// Использование
DeadlockWithTimeout example = new DeadlockWithTimeout();
for (int i = 0; i < 5; i++) {
if (!example.transferMoney()) {
System.out.println("Deadlock обнаружен, повторная попытка...");
Thread.sleep(100); // Подождали и попробовали снова
}
}
3. ReadWriteLock для уменьшения конкурентности
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
private int value = 0;
// Множество читателей одновременно
public int read() {
rwLock.readLock().lock();
try {
return value;
} finally {
rwLock.readLock().unlock();
}
}
// Писатель блокирует всех
public void write(int newValue) {
rwLock.writeLock().lock();
try {
value = newValue;
} finally {
rwLock.writeLock().unlock();
}
}
}
// Результат: меньше блокировок, меньше шансов на deadlock
4. Использование ConcurrentCollections
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentExample {
// ❌ Может быть deadlock
private Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// ✓ Нет deadlock (internal locking более продвинуто)
private ConcurrentMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
public void process() {
// ConcurrentHashMap использует segment-based locking
// Не блокирует весь map, только один сегмент
concurrentMap.put("key", 100);
concurrentMap.get("key");
}
}
5. StampedLock (Java 8+) для высокой производительности
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private double x, y;
private final StampedLock sl = new StampedLock();
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // Оптимистичное чтение
double currentX = x;
double currentY = y;
// Если данные изменились во время чтения
if (!sl.validate(stamp)) {
// Перечитываем с полной блокировкой
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
}
6. Actor Model (Akka) для избежания синхронизации
// Вместо shared state + синхронизации
// используем message passing (Akka Actors)
// Каждый Actor имеет свой mailbox
// Сообщения обрабатываются последовательно
// Нет shared state → нет deadlock
Detecting Deadlock (обнаружение)
// 1. JVM предоставляет инструменты
jps -l // Список Java процессов
jstack <pid> // Dump потоков и locks
// 2. В jstack выглядит так:
// "Thread-1": waiting to lock monitor 0x...
// "Thread-0": holding lock 0x...
// → DEADLOCK обнаружен!
Чеклист для избежания Deadlock
✓ Захватывайте ресурсы в одном и том же порядке ✓ Используйте таймауты (tryLock вместо lock) ✓ Минимизируйте время удержания ресурсов ✓ Используйте ConcurrentCollections вместо synchronized ✓ Предпочитайте immutable объекты ✓ Логируйте и мониторьте потоки ✓ Используйте tool'ы (JProfiler, YourKit) для обнаружения
Реальный пример: правильная банковская транзакция
@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
// Правило: всегда сначала ID с меньшим значением
if (from.getId() < to.getId()) {
synchronized (from) {
synchronized (to) {
from.debit(amount);
to.credit(amount);
}
}
} else {
synchronized (to) {
synchronized (from) {
from.debit(amount);
to.credit(amount);
}
}
}
// NO DEADLOCK ✓
}
Вывод
Deadlock — это серьёзная проблема, но она полностью избежима:
- Основное правило: захватывайте ресурсы в одинаковом порядке
- Таймауты: используйте
tryLock()вместоlock() - Concurrent коллекции: ConcurrentHashMap вместо synchronized
- Минимизируйте синхронизацию: используйте immutable объекты
- Мониторьте: используйте tools для обнаружения deadlocks
В современной Java (Stream API, CompletableFuture, reactive frameworks) часто вообще избегают необходимости явной синхронизации, что естественным образом предотвращает deadlocks.