Понимание тупика на простом примере
Я работаю над пониманием основ взаимоблокировки, поэтому я придумал код ниже. У меня есть два потока, получающие блокировки в противоположном порядке, но они не блокируются. Когда я запускаю его, я вижу все распечатки. Что я делаю неправильно?
public class DeadlockBasics {
private Lock lockA = new ReentrantLock();
private Lock lockB = new ReentrantLock();
public static void main(String[] args) {
DeadlockBasics dk = new DeadlockBasics();
dk.execute();
}
private void execute() {
new Thread(this::processThis).start();
new Thread(this::processThat).start();
}
// called by thread 1
public void processThis() {
lockA.lock();
// process resource A
System.out.println("resource A -Thread1");
lockB.lock();
// process resource B
System.out.println("resource B -Thread1");
lockA.unlock();
lockB.unlock();
}
// called by thread 2
public void processThat() {
lockB.lock();
// process resource B
System.out.println("resource B -Thread2");
lockA.lock();
// process resource A
System.out.println("resource A -Thread2");
lockA.unlock();
lockB.unlock();
}
}
4 ответа
Прежде всего, нет гарантии, какие потоки запускаются первыми. Чтобы получить тупик, один из потоков должен взять блокировку на lockA
а затем второй поток должен взять блокировку на lockB
или наоборот.
public void processThis() {
lockA.lock();
// here the control should be switched to another thread
System.out.println("resource A -Thread1");
lockB.lock();
...
Но может не хватить времени для переключения между потоками, потому что у вас всего несколько строк кода. Это слишком быстро. Чтобы эмулировать долгую работу, добавьте задержку перед второй блокировкой для обоих методов.
lockA.lock();
Thread.sleep(200); // 200 milis
Тогда второй поток сможет заблокировать lockB
до первого релиза оба
Это действительно может привести к взаимоблокировке, но не всегда, например, если processThis () полностью выполняется, а затем processThat () или наоборот, тупиковая ситуация отсутствует. Вы можете попробовать добавить Thread.delay(100) или Thread.yield (), чтобы направить выполнение потоков в тупик или даже снять разблокировку с определенного тупика.
Два момента:
- Отпустите замки в порядке, обратном их приобретению. То есть,
processThis
следует поменять порядок снятия замков. Для вашего примера порядок не имеет значения. Но еслиprocessThis
попытался установить новую блокировку на A, прежде чем снять блокировку на B, снова может произойти тупик. В более общем плане вам будет проще думать о блокировках, учитывая их объем и избегая перекрывающихся, но не ограничивающих область. - Чтобы лучше выделить проблему, я бы позвонил
wait
после получения первой блокировки в каждом из потоков, и иметьexecute
запустить оба потока, затем вызватьnotify
на обе темы.
Ваш код является хорошим примером мертвой блокировки, поскольку ReenttrantLock - это блокировка взаимного исключения с тем же поведением, что и неявная блокировка доступа к монитору с использованием синхронизированного. Однако вы не видите тупик из-за этой части:
private void execute() {
new Thread(this::processThis).start();
new Thread(this::processThat).start();
}
После создания и запуска первого потока создание второго потока занимает некоторое время. Для создания новой нити JVM требуется около 50, а может и меньше, звучит очень коротко, но этого достаточно, чтобы завершить первую нить, и, следовательно, не будет мертвой блокировки.
Я добавил Thread.sleep();
в ваш код, чтобы оба потока могли выполняться как-то параллельно.
package com.company;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockBasics {
private Lock lockA = new ReentrantLock();
private Lock lockB = new ReentrantLock();
public static void main(String[] args) {
DeadlockBasics dk = new DeadlockBasics();
dk.execute();
}
private void execute() {
new Thread(this::processThis).start();
new Thread(this::processThat).start();
}
// called by thread 1
private void processThis() {
lockA.lock();
// process resource A
try {
Thread.sleep(1000); //Wait for thread 2 to be executed
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 will own lock a");
lockB.lock();
// process resource B
System.out.println("Thread 1 will own lock b");
lockA.unlock();
lockB.unlock();
// Both locks will now released from thread 1
}
// called by thread 2
private void processThat() {
lockB.lock();
// process resource B
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 will own lock b");
lockA.lock();
// process resource A
System.out.println("Thread 2 will own lock a");
lockA.unlock();
lockB.unlock();
// Both locks are released by thread 2
}
}