Deadlock (Взаимная блокировка) — это ситуация в многопоточном программировании, когда два или более потоков блокируют друг друга, ожидая освобождения ресурсов, которые удерживаются этими же потоками. В результате потоки не могут продолжить выполнение, и программа "зависает". Deadlock является одной из самых распространенных проблем в многопоточных приложениях.
Для возникновения взаимной блокировки необходимо выполнение четырех условий, известных как условия Коффмана:
Рассмотрим классический пример Deadlock, где два потока блокируют друг друга:
public class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("Поток 1 удерживает lock1");
try {
Thread.sleep(100); // Имитация работы
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Поток 1 удерживает lock2");
}
}
}
public void method2() {
synchronized (lock2) {
System.out.println("Поток 2 удерживает lock2");
try {
Thread.sleep(100); // Имитация работы
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Поток 2 удерживает lock1");
}
}
}
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
Thread thread1 = new Thread(example::method1);
Thread thread2 = new Thread(example::method2);
thread1.start();
thread2.start();
}
}
lock1
и пытается захватить lock2
.lock2
и пытается захватить lock1
.Один из самых эффективных способов избежать Deadlock — это всегда получать блокировки в определенном порядке. Если все потоки будут захватывать ресурсы в одинаковом порядке, круговая зависимость не возникнет.
Пример:
public class NoDeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("Поток 1 удерживает lock1");
try {
Thread.sleep(100); // Имитация работы
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Поток 1 удерживает lock2");
}
}
}
public void method2() {
synchronized (lock1) { // Теперь оба метода захватывают lock1 первым
System.out.println("Поток 2 удерживает lock1");
try {
Thread.sleep(100); // Имитация работы
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Поток 2 удерживает lock2");
}
}
}
public static void main(String[] args) {
NoDeadlockExample example = new NoDeadlockExample();
Thread thread1 = new Thread(example::method1);
Thread thread2 = new Thread(example::method2);
thread1.start();
thread2.start();
}
}
Использование тайм-аутов при попытке захватить блокировку позволяет избежать бесконечного ожидания. Если поток не может захватить блокировку в течение определенного времени, он может освободить уже захваченные ресурсы и попробовать снова.
Пример с использованием ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void method1() {
while (true) {
if (lock1.tryLock()) {
try {
System.out.println("Поток 1 удерживает lock1");
if (lock2.tryLock()) {
try {
System.out.println("Поток 1 удерживает lock2");
break; // Успешно захватили оба ресурса
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
try {
Thread.sleep(100); // Пауза перед повторной попыткой
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2() {
while (true) {
if (lock2.tryLock()) {
try {
System.out.println("Поток 2 удерживает lock2");
if (lock1.tryLock()) {
try {
System.out.println("Поток 2 удерживает lock1");
break; // Успешно захватили оба ресурса
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
try {
Thread.sleep(100); // Пауза перед повторной попыткой
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TimeoutExample example = new TimeoutExample();
Thread thread1 = new Thread(example::method1);
Thread thread2 = new Thread(example::method2);
thread1.start();
thread2.start();
}
}
Для управления сложными сценариями синхронизации можно использовать высокоуровневые инструменты, такие как java.util.concurrent
пакет, который предоставляет классы Semaphore
, CountDownLatch
, CyclicBarrier
и другие. Эти инструменты помогают избежать Deadlock, предоставляя более гибкие механизмы синхронизации.
java.util.concurrent
для управления синхронизацией.Соблюдение этих принципов поможет вам избежать Deadlock и создавать более надежные многопоточные приложения.