Fehlerbehebung bei Deadlocks in Java-Threads

Deadlocks in Java sind eines der herausforderndsten Probleme, die es in Multithread-Anwendungen zu debuggen gilt. In diesem Artikel werden wir untersuchen, was Deadlocks sind, wie sie in Java erstellt und erkannt werden, und Strategien aufzeigen, wie Sie diese in Ihren Anwendungen verhindern können.

Was ist ein Deadlock?

Ein Deadlock tritt in einer Multithread-Umgebung auf, wenn zwei oder mehr Threads für immer blockiert sind, wobei jeder auf die Freigabe einer Ressource durch den anderen wartet. Diese Situation führt dazu, dass das Programm einfriert, da keiner der Threads fortfahren kann. Deadlocks entstehen oft, wenn mehrere Threads versuchen, Sperren auf gemeinsam genutzte Ressourcen zu setzen, wodurch eine zirkuläre Abhängigkeit entsteht.

Sehen wir uns ein einfaches Java-Programm an, um zu demonstrieren, wie ein Deadlock in der Praxis auftreten kann.

Beispielprogramm zur Erstellung eines Deadlocks

 
package com.example.threads;

public class ThreadDeadlock {

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();
    
        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");
        
        t1.start();
        Thread.sleep(5000);
        t2.start();
        Thread.sleep(5000);
        t3.start();
    }
}

class SyncThread implements Runnable {
    private Object obj1;
    private Object obj2;

    public SyncThread(Object o1, Object o2) {
        this.obj1 = o1;
        this.obj2 = o2;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on " + obj1);
        synchronized (obj1) {
            System.out.println(name + " acquired lock on " + obj1);
            work();
            System.out.println(name + " acquiring lock on " + obj2);
            synchronized (obj2) {
                System.out.println(name + " acquired lock on " + obj2);
                work();
            }
            System.out.println(name + " released lock on " + obj2);
        }
        System.out.println(name + " released lock on " + obj1);
        System.out.println(name + " finished execution.");
    }

    private void work() {
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

In diesem Programm arbeitet die SyncThread-Klasse mit zwei gemeinsam genutzten Objekten, indem sie mit einem synchronisierten Block Sperren erwirbt. Drei Threads versuchen, auf die gleichen Ressourcen (obj1, obj2 und obj3) zuzugreifen. Aufgrund der Reihenfolge, in der die Sperren erworben werden, führt das Programm zu einem Deadlock, da jeder Thread endlos darauf wartet, dass die anderen Threads die Sperren freigeben.

Beispielausgabe

Wenn Sie dieses Programm ausführen, könnte es folgende Ausgabe erzeugen:

 
t1 acquiring lock on java.lang.Object@6d9dd520
t1 acquired lock on java.lang.Object@6d9dd520
t2 acquiring lock on java.lang.Object@22aed3a5
t2 acquired lock on java.lang.Object@22aed3a5
t3 acquiring lock on java.lang.Object@218c2661
t3 acquired lock on java.lang.Object@218c2661
t1 acquiring lock on java.lang.Object@22aed3a5
t2 acquiring lock on java.lang.Object@218c2661
t3 acquiring lock on java.lang.Object@6d9dd520

Hier stecken die Threads in einer Warteschleife fest, da sie jeweils aufeinander warten, was zu einem Deadlock führt.

Erkennung von Deadlocks

In realen Anwendungen kann die Erkennung von Deadlocks komplexer sein. Eine übliche Methode zur Erkennung von Deadlocks besteht darin, einen Thread-Dump zu erzeugen, der den Zustand aller Threads in der Anwendung anzeigt. Hier ist ein Ausschnitt aus einem Thread-Dump, der einen Deadlock veranschaulicht:

 
Found one Java-level deadlock:
=============================
"t3":
  waiting to lock monitor 0x00007fb0a1074b08 (object 0x000000013df2f658, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x00007fb0a1010f08 (object 0x000000013df2f668, a java.lang.Object),
  which is held by "t2"
"t2":
  waiting to lock monitor 0x00007fb0a1012360 (object 0x000000013df2f678, a java.lang.Object),
  which is held by "t3"

Im obigen Dump können Sie sehen, dass jeder Thread auf die Freigabe einer Sperre durch einen anderen Thread wartet, wodurch eine zirkuläre Abhängigkeit entsteht – das klassische Deadlock-Szenario.

Vermeidung von Deadlocks in Java

Während es möglich ist, Deadlocks zu erkennen, ist es weitaus besser, sie von vornherein zu vermeiden. Hier sind einige Strategien, um Deadlocks in Ihren Java-Anwendungen zu verhindern:

Vermeidung verschachtelter Sperren

Die häufigste Ursache für Deadlocks sind verschachtelte Sperren. Vermeiden Sie es, mehrere Sperren gleichzeitig zu erwerben. Wenn Sie mehrere Sperren erwerben müssen, stellen Sie sicher, dass alle Threads sie in der gleichen Reihenfolge erwerben.

 
   synchronized (obj1) {
       // Perform work
   }
   synchronized (obj2) {
       // Perform work
   }

Verwenden eines Zeitlimits für den Thread-Join

Vermeiden Sie unbestimmtes Warten. Wenn ein Thread auf das Ende eines anderen Threads warten muss, verwenden Sie ein Zeitlimit für join(), um endloses Blockieren zu verhindern.

 
   t1.join(1000);  // Warte maximal 1 Sekunde, bis t1 beendet ist

Verwendung von `tryLock`

Wenn Sie die Lock-Schnittstelle verwenden, sollten Sie die Methode tryLock() in Betracht ziehen, die versucht, die Sperre zu erwerben, ohne unendlich lange zu blockieren.

 
   if (lock1.tryLock()) {
       try {
           // Perform work
       } finally {
           lock1.unlock();
       }
   }

Sperren Sie nur das Notwendige

Minimieren Sie den Umfang der synchronisierten Blöcke, um unnötige Ressourcensperren zu vermeiden. Sperren Sie nur das, was Sie wirklich benötigen.

Durch die Anwendung dieser Praktiken können Sie die Wahrscheinlichkeit von Deadlocks in Ihren Anwendungen erheblich reduzieren.

Fazit

Deadlocks sind eine herausfordernde Angelegenheit in der Java-Multithreading-Welt, aber das Verständnis der Ursachen und die frühzeitige Erkennung sind der Schlüssel zur Aufrechterhaltung effizienter Anwendungen. Wenn Sie bewährte Praktiken wie das Vermeiden verschachtelter Sperren und die Verwendung von Zeitlimits anwenden, können Sie Deadlocks verhindern. Denken Sie daran, dass das Debuggen eines Deadlocks eine gründliche Analyse der Thread-Zustände und Abhängigkeiten erfordert, daher ist es immer vorteilhaft, von Anfang an sauberen, synchronisierten Code zu schreiben.

Kostenlosen Account erstellen

Registrieren Sie sich jetzt und erhalten Sie Zugang zu unseren Cloud Produkten.

Das könnte Sie auch interessieren:

centron Managed Cloud Hosting in Deutschland

Java-Array: So prüfst du Werte effizient

JavaScript
Wie prüft man, ob ein Java Array einen Wert enthält? Es gibt viele Möglichkeiten, um zu überprüfen, ob ein Java Array einen bestimmten Wert enthält. Einfache Iteration mit einer for-Schleife…