Найдите причину дедлока в данной Java-программе и предложите варианты его устранения: Object a = new Object(), b = new Object(); Thread t1 = new Thread(() -> { synchronized(a) { Thread.sleep(50); synchronized(b){} } }); Thread t2 = new Thread(() -> { synchronized(b) { Thread.sleep(50); synchronized(a){} } }); — обсудите порядок блокировок и альтернативы (tryLock, lock ordering)
Причина дедлока - Сценарий: поток t1t1t1 захватывает монитор объекта aaa и ждёт монитор bbb; t2t2t2 захватывает монитор bbb и ждёт aaa. Получается циклическое ожидание (mutual hold-and-wait) — классический дедлок. Thread.sleep(505050) не создаёт дедлок сам по себе, но увеличивает окно, в котором другое число может захватить другой ресурс. Варианты устранения (кратко, с примерами) 1) Единый порядок захвата (lock ordering) - Правило: все потоки должны захватывать несколько замков в одном и том же порядке. - Пример: оба потока всегда делают synchronized(a) затем synchronized(b). Код: synchronized(a) { Thread.sleep(505050); synchronized(b) { ... } } - Универсальный вариант (по сравнению identityHashCode): Object first = a, second = b; if (System.identityHashCode(a) > System.identityHashCode(b)) { first = b; second = a; } synchronized(first) { synchronized(second) { ... } } 2) Попытка захвата с откатом (tryLock с таймаутом) - Использовать ReentrantLock и tryLock(timeouttimeouttimeout, TimeUnit.MILLISECONDS): если захват второго замка не удался — отпустить первый, подождать и повторить. - Пример (схематично): ReentrantLock la, lb; if (la.tryLock(100100100, TimeUnit.MILLISECONDS)) { try { if (lb.tryLock(100100100, TimeUnit.MILLISECONDS)) { try { /* критическая секция */ } finally { lb.unlock(); } } else { /* откат — отпустить la и ретрай */ } } finally { la.unlock(); } } 3) Избежать удержания нескольких замков - Объединить данные под одним замком (если допустимо) — проще и безопасно. - Использовать структуры из java.util.concurrent (ConcurrentHashMap, Atomic… ), которые уменьшают необходимость внешних мониторов. 4) Минимизировать время удержания замка - Не вызывать Thread.sleep или долгие операции внутри synchronized; вычисления/IO делать вне критической секции. 5) Высокоуровневые абстракции - Использовать Actor-модель, queues, CompletableFuture, или синхронизаторы (Semaphore, StampedLock) — в зависимости от задачи это может устранить необходимость в прямых блокировках. Дополнительно: для диагностики использовать thread dump / jstack — он покажет, кто держит какие мониторы. Резюме: самый простой и надёжный — привести порядок захвата замков к единому (lock ordering). tryLock с таймаутом даёт гибкую защиту от дедлока, но требует корректной обработки откатов и ретраев.
- Сценарий: поток t1t1t1 захватывает монитор объекта aaa и ждёт монитор bbb; t2t2t2 захватывает монитор bbb и ждёт aaa. Получается циклическое ожидание (mutual hold-and-wait) — классический дедлок. Thread.sleep(505050) не создаёт дедлок сам по себе, но увеличивает окно, в котором другое число может захватить другой ресурс.
Варианты устранения (кратко, с примерами)
1) Единый порядок захвата (lock ordering)
- Правило: все потоки должны захватывать несколько замков в одном и том же порядке.
- Пример: оба потока всегда делают synchronized(a) затем synchronized(b).
Код: synchronized(a) { Thread.sleep(505050); synchronized(b) { ... } }
- Универсальный вариант (по сравнению identityHashCode):
Object first = a, second = b;
if (System.identityHashCode(a) > System.identityHashCode(b)) { first = b; second = a; }
synchronized(first) { synchronized(second) { ... } }
2) Попытка захвата с откатом (tryLock с таймаутом)
- Использовать ReentrantLock и tryLock(timeouttimeouttimeout, TimeUnit.MILLISECONDS): если захват второго замка не удался — отпустить первый, подождать и повторить.
- Пример (схематично):
ReentrantLock la, lb;
if (la.tryLock(100100100, TimeUnit.MILLISECONDS)) {
try {
if (lb.tryLock(100100100, TimeUnit.MILLISECONDS)) {
try { /* критическая секция */ }
finally { lb.unlock(); }
} else { /* откат — отпустить la и ретрай */ }
} finally { la.unlock(); }
}
3) Избежать удержания нескольких замков
- Объединить данные под одним замком (если допустимо) — проще и безопасно.
- Использовать структуры из java.util.concurrent (ConcurrentHashMap, Atomic… ), которые уменьшают необходимость внешних мониторов.
4) Минимизировать время удержания замка
- Не вызывать Thread.sleep или долгие операции внутри synchronized; вычисления/IO делать вне критической секции.
5) Высокоуровневые абстракции
- Использовать Actor-модель, queues, CompletableFuture, или синхронизаторы (Semaphore, StampedLock) — в зависимости от задачи это может устранить необходимость в прямых блокировках.
Дополнительно: для диагностики использовать thread dump / jstack — он покажет, кто держит какие мониторы.
Резюме: самый простой и надёжный — привести порядок захвата замков к единому (lock ordering). tryLock с таймаутом даёт гибкую защиту от дедлока, но требует корректной обработки откатов и ретраев.