Vorwort
Deadlock ist ein häufiges Problem bei der gleichzeitigen Programmierung, das auftritt, wenn zwei oder mehr Threads blockiert sind und darauf warten, dass der andere eine Sperre aufhebt. Ein Deadlock kann dazu führen, dass das gesamte System einfriert oder abstürzt, was ein schwierig zu reproduzierendes und zu behebendes Problem darstellt. In diesem Artikel untersuchen wir die Ursachen von Deadlocks in Java, wie man sie erkennt und bewährte Methoden zur Vermeidung von Deadlocks.
Was ist Deadlock?
Ein Deadlock liegt in Java vor, wenn zwei oder mehr Threads blockiert sind und darauf warten, dass der andere Threads Ressourcen freigibt. Diese Situation wird Deadlock genannt. Mit anderen Worten: Zwei oder mehr Threads bleiben hängen und können nicht fortfahren, da jeder Thread eine Ressource enthält, die der andere Thread benötigt, was zu einer zirkulären Abhängigkeit führt. Dies kann dazu führen, dass das System vollständig einfriert oder abstürzt.
Betrachten Sie beispielsweise zwei Threads, Thread A und Thread B, und zwei Sperren, lock1 und lock2. Thread A erhält Sperre 1 und Thread B erhält Sperre 2. Thread A benötigt jedoch Sperre 2, um fortzufahren, und Thread B benötigt Sperre 1, um die Ausführung fortzusetzen, die von Thread A gehalten wird. Dies führt zu einer zirkulären Abhängigkeit, bei der beide Threads blockiert sind und darauf warten, dass der andere Thread die Sperre aufhebt. Diese Situation wird als Deadlock bezeichnet.
Schauen wir uns einen Code direkt an:
package core.multithreading;
public class DeadlockExample {
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread A acquired lock 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized(lock2) {
System.out.println("Thread A acquired lock 2");
}
}
});
Thread threadB = new Thread(() -> {
synchronized(lock2) {
System.out.println("Thread B acquired lock 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized(lock1) {
System.out.println("Thread B acquired lock 1");
}
}
});
threadA.start();
threadB.start();
}
}
In diesem Beispiel haben wir zwei Threads threadA
und threadB
, die beide auf zwei Sperren zugreifen, lock1
und lock2
. threadA
Zuerst „Acquire“ lock1
und dann threadB
„Acquire“ lock2
. Beide Threads ruhen eine Sekunde lang. Versuchen Sie dann threadA
zu bekommen , threadB
was gehalten wird lock2
, threadB
versuchen Sie zu bekommen, threadA
was gehalten wird lock1
. Dies führt zu einer zirkulären Abhängigkeit, bei der beide Threads blockiert sind und darauf warten, dass der andere Thread die Sperre aufhebt, was zu einem Deadlock führt.
Um Deadlocks wie diesen zu vermeiden, können Sie Best Practices für die gleichzeitige Programmierung befolgen, z. B. das Erlangen von Sperren in einer festen Reihenfolge, die Verwendung von Zeitüberschreitungen beim Erlangen von Sperren, das Minimieren des Umfangs von Sperren und die Verwendung derjenigen im Paket juc ReentrantLock
.
Wie erkennt man einen Deadlock in Java?
Das Erkennen von Deadlocks kann eine schwierige Aufgabe sein, da das System scheinbar eingefroren ist oder nicht reagiert und nicht klar ist, wo das Problem liegt. Glücklicherweise bietet Java integrierte Tools zum Erkennen und Diagnostizieren von Deadlocks.
- Dump-Thread-Informationen
Mithilfe der Thread-Dump-Analyse können Deadlocks in Java erkannt werden. Ein Thread-Dump ist eine Momentaufnahme des Status aller Threads, die zu einem bestimmten Zeitpunkt in einer Java Virtual Machine (JVM) ausgeführt werden. Durch die Analyse von Thread-Dumps können Sie erkennen, ob ein Deadlock aufgetreten ist. In einem Thread-Dump können Sie Threads finden, die blockiert sind und auf Sperren warten, und feststellen, welche Sperren von welchen Threads gehalten werden. Wenn Sie in der Sperrreihenfolge eine zirkuläre Abhängigkeit sehen, ist dies ein Zeichen für einen möglichen Deadlock.
Hier ist ein Beispiel-Thread-Dump, der einen möglichen Deadlock zeigt:
"Thread 1" - waiting to lock monitor on Lock 1
"Thread 2" - waiting to lock monitor on Lock 2
Found 1 deadlock.
- JConsole
JConsole
ist ein Java Management Extensions (JMX)-Client, mit dem Sie Java-Anwendungen überwachen und verwalten können. Sie JConsole
können Threads
Deadlocks mithilfe der Registerkarte „Überprüfung“ erkennen. Wenn ein Thread blockiert ist und auf eine Sperre wartet, wird er in der Thread State
Spalte „ “ mit dem Wert „ BLOCKED
“ angezeigt.
Hier ist JConsole
ein Beispiel :
Name: Thread-1
State: BLOCKED on Lock 1
- VisualVM
VisualVM
ist ein weiteres Tool, mit dem Sie Java
Anwendungen . Wie JConsole
bei können Sie VisualVM
Deadlocks erkennen, indem Sie die Registerkarte „Threads“ überprüfen. Wenn ein Thread blockiert ist und auf eine Sperre wartet, wird er in der State
Spalte „ “ mit dem Wert „ BLOCKED
“ angezeigt.
Hier ist VisualVM
ein Beispiel :
Name: Thread-1
State: BLOCKED on Lock 1 owned by Thread-2
- LockSupport
LockSupport
Die Klasse stellt eine Reihe statischer Methoden bereit, mit denen Deadlocks erkannt werden können. Eine der Methoden besteht darin, dass parkNanos()
damit überprüft werden kann, ob ein Thread blockiert ist und auf eine Sperre wartet. Wenn „true“ parkNanos()
zurückgegeben wird , bedeutet dies, dass der Thread blockiert ist und ein potenzieller Deadlock vorliegt.
Hier ist ein Beispiel für die Verwendung LockSupport
zur Erkennung potenzieller Deadlocks:
Thread t = Thread.currentThread();
LockSupport.parkNanos(1000000000);
if (t.getState() == Thread.State.BLOCKED) {
// Potential deadlock
}
Best Practices zur Vermeidung von Deadlocks
- Erwerben Sie Schlösser in einer festen Reihenfolge
Um zirkuläre Abhängigkeitsketten zu vermeiden, sollten Sie Sperren in einer festen Reihenfolge erwerben. Das heißt, wenn zwei oder mehr Threads mehrere Sperren erwerben müssen, sollten sie diese immer in derselben Reihenfolge erwerben. Wenn Thread A beispielsweise die Sperre X und dann die Sperre Y erhält, sollte Thread B die Sperre X erwerben, bevor er versucht, die Sperre Y zu erwerben.
Hier ist ein Beispielcode, der Sperren in einer festen Reihenfolge erhält, um zirkuläre Abhängigkeiten zu vermeiden:
package core.multithreading;
public class DeadlockExample {
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread A acquired lock 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized(lock2) {
System.out.println("Thread A acquired lock 2");
}
}
});
Thread threadB = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread B acquired lock 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized(lock2) {
System.out.println("Thread B acquired lock 1");
}
}
});
threadA.start();
threadB.start();
}
}
lock1
In diesem Beispiel haben wir zwei Threads, von denen jeder eine Methode aufruft und zwei Sperren ( und lock2
) in einer festen Reihenfolge erhält . Beide Methoden erwerben Sperren in derselben Reihenfolge: zuerst lock1
, dann lock2
. Dadurch wird sichergestellt, dass keine zirkulären Abhängigkeiten zwischen Sperren bestehen.
- Verwenden Sie beim Erwerb einer Sperre eine Zeitüberschreitung
Um Deadlocks zu vermeiden, können Sie beim Erwerb von Sperren Timeouts verwenden. Dies bedeutet, dass der Thread die Sperre aufhebt und es später erneut versucht, wenn die Sperre nicht innerhalb der angegebenen Zeit erworben werden kann.
package core.multithreading;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTimeoutExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void method1() {
boolean lock1Acquired = false;
boolean lock2Acquired = false;
try {
System.out.println("Thread 1: Attempting to acquire lock1");
lock1Acquired = lock1.tryLock(500, TimeUnit.MILLISECONDS);
System.out.println("Thread 1: Acquired lock1 = " + lock1Acquired);
System.out.println("Thread 1: Attempting to acquire lock2");
lock2Acquired = lock2.tryLock(500, TimeUnit.MILLISECONDS);
System.out.println("Thread 1: Acquired lock2 = " + lock2Acquired);
if (lock1Acquired && lock2Acquired) {
// Do something
} else {
// Locks not acquired
}
} catch (InterruptedException e) {
// Handle the exception
} finally {
if (lock1Acquired) {
lock1.unlock();
System.out.println("Thread 1: Released lock1");
}
if (lock2Acquired) {
lock2.unlock();
System.out.println("Thread 1: Released lock2");
}
}
}
public void method2() {
boolean lock1Acquired = false;
boolean lock2Acquired = false;
try {
System.out.println("Thread 2: Attempting to acquire lock2");
lock2Acquired = lock2.tryLock(500, TimeUnit.MILLISECONDS);
System.out.println("Thread 2: Acquired lock2 = " + lock2Acquired);
System.out.println("Thread 2: Attempting to acquire lock1");
lock1Acquired = lock1.tryLock(500, TimeUnit.MILLISECONDS);
System.out.println("Thread 2: Acquired lock1 = " + lock1Acquired);
if (lock1Acquired && lock2Acquired) {
// Do something
} else {
// Locks not acquired
}
} catch (InterruptedException e) {
// Handle the exception
} finally {
if (lock1Acquired) {
lock1.unlock();
System.out.println("Thread 2: Released lock1");
}
if (lock2Acquired) {
lock2.unlock();
System.out.println("Thread 2: Released lock2");
}
}
}
public static void main(String[] args) {
LockTimeoutExample example = new LockTimeoutExample();
Thread t1 = new Thread(new Runnable() {
public void run() {
example.method1();
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
example.method2();
}
});
t1.start();
t2.start();
}
}
In diesem Beispiel erstellen wir eine Instanz der DeadlockExample
Klasse und starten zwei Threads, von denen einer ausgeführt wird method1()
und der andere ausgeführt wird method2()
. Jede Methode versucht, die beiden Sperren in einer anderen Reihenfolge zu erhalten, was das Auftreten von Deadlocks verhindern sollte.
- Minimieren Sie den Umfang der Sperre
Um Deadlocks zu vermeiden, sollten Sie den Umfang Ihrer Sperren minimieren. Das bedeutet, dass Sie die Sperre nur bei Bedarf in Anspruch nehmen und so schnell wie möglich wieder freigeben sollten. Dies kann durch die Verwendung synchronisierter Blöcke anstelle synchronisierter Methoden erreicht werden. Mit einem synchronisierten Block können Sie den Umfang der Sperre explizit angeben.
package core.multithreading;
public class SynchronizedLockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("method1: lock1 acquired");
try {
Thread.sleep(1000); // simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("method1: lock2 acquired");
// Do something
}
}
}
public void method2() {
synchronized (lock1) {
System.out.println("method2: lock1 acquired");
try {
Thread.sleep(1000); // simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("method2: lock2 acquired");
// Do something
}
}
}
public static void main(String[] args) {
SynchronizedLockExample example = new SynchronizedLockExample();
Thread t1 = new Thread(new Runnable() {
public void run() {
example.method1();
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
example.method2();
}
});
t1.start();
t2.start();
}
}
In method1
und werden Blöcke zum Erlangen bzw. Sperren method2
von Sperren synchronized
verwendet . In der Methode werden zwei Threads erstellt, um die beiden Methoden aufzurufen. Wenn die Threads mit der Ausführung beginnen, erhält ein Thread die Sperre und der andere Thread wartet, bis die Sperre aufgehoben wird. Sobald die Sperre aufgehoben wird, erhält der wartende Thread die Sperre und fährt mit der Ausführung des inneren Blocks fort.lock1
lock2
main
lock1
method2
synchronized
Zusammenfassen
Deadlocks sind ein häufiges Problem bei der gleichzeitigen Programmierung und können dazu führen, dass ein System vollständig einfriert oder abstürzt. Das Erkennen und Beheben von Deadlocks kann eine anspruchsvolle Aufgabe sein, aber Java bietet integrierte Tools zum Erkennen und Diagnostizieren von Deadlocks. Um Deadlocks zu vermeiden, sollten Sie Sperren in einer festen Reihenfolge erwerben, Zeitüberschreitungen beim Erwerb von Sperren verwenden und den Umfang der Sperren minimieren. Durch Befolgen dieser Best Practices können Sie das Risiko von Deadlocks verringern und sicherstellen, dass Ihre gleichzeitigen Programme reibungslos laufen.