Тупик трапляється, коли потоки (або те, що ваша платформа називає своїми блоками виконання) отримують ресурси, де кожен ресурс може одночасно утримуватися лише одним потоком, і він утримується на цих ресурсах таким чином, що утримання не можна попереджувати, і існує деяка "кругова" взаємозв'язок між потоками, така що кожен потік у тупиковій ситуації чекає на отримання ресурсу, який утримується іншим потоком.
Отже, найпростіший спосіб уникнути глухого кута - це надати загальний порядок ресурсам і нав'язати правило, згідно з яким ресурси отримуються лише потоками в порядку . І навпаки, глухий кут можна навмисно створити, запустивши потоки, які отримують ресурси, але не отримують їх по порядку. Наприклад:
Дві нитки, два замки. Перший потік запускає цикл, який намагається отримати замки в певному порядку, другий потік запускає цикл, який намагається отримати замки в протилежному порядку. Кожна нитка звільняє обидва замки після успішного отримання замків.
public class HighlyLikelyDeadlock {
static class Locker implements Runnable {
private Object first, second;
Locker(Object first, Object second) {
this.first = first;
this.second = second;
}
@Override
public void run() {
while (true) {
synchronized (first) {
synchronized (second) {
System.out.println(Thread.currentThread().getName());
}
}
}
}
}
public static void main(final String... args) {
Object lock1 = new Object(), lock2 = new Object();
new Thread(new Locker(lock1, lock2), "Thread 1").start();
new Thread(new Locker(lock2, lock1), "Thread 2").start();
}
}
Зараз у цьому питанні було кілька коментарів, які вказують на різницю між ймовірністю та певністю тупикової ситуації. У якомусь сенсі ця відмінність є академічним питанням. З практичної точки зору, я, безумовно, хотів би бачити працюючу систему, яка не забивається в коді, написаному мною вище :)
Однак питання співбесіди часом можуть бути академічними, і це ТА питання має в назві слово "напевно", тож далі йде програма, яка, безумовно, тупикова. Створюються два Locker
об'єкти, кожен отримує два блокування та CountDownLatch
використовується для синхронізації між потоками. Кожен Locker
замикає перший замок, після чого відраховує засувку один раз. Коли обидві нитки отримали замок і відрахували засувку, вони проходять повз заслінку замку і намагаються отримати другий замок, але в кожному випадку інша нитка вже тримає бажаний замок. Ця ситуація призводить до певного глухого кута.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CertainDeadlock {
static class Locker implements Runnable {
private CountDownLatch latch;
private Lock first, second;
Locker(CountDownLatch latch, Lock first, Lock second) {
this.latch = latch;
this.first = first;
this.second = second;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
try {
first.lock();
latch.countDown();
System.out.println(threadName + ": locked first lock");
latch.await();
System.out.println(threadName + ": attempting to lock second lock");
second.lock();
System.out.println(threadName + ": never reached");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(final String... args) {
CountDownLatch latch = new CountDownLatch(2);
Lock lock1 = new ReentrantLock(), lock2 = new ReentrantLock();
new Thread(new Locker(latch, lock1, lock2), "Thread 1").start();
new Thread(new Locker(latch, lock2, lock1), "Thread 2").start();
}
}