Deadlock-Erkennung und -Verhinderung in Java

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 threadAund threadB, die beide auf zwei Sperren zugreifen, lock1und lock2. threadAZuerst „Acquire“ lock1und dann threadB„Acquire“ lock2. Beide Threads ruhen eine Sekunde lang. Versuchen Sie dann threadAzu bekommen , threadBwas gehalten wird lock2, threadBversuchen Sie zu bekommen, threadAwas 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.

  1. 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.
  1. JConsole

JConsoleist ein Java Management Extensions (JMX)-Client, mit dem Sie Java-Anwendungen überwachen und verwalten können. Sie JConsolekönnen ThreadsDeadlocks mithilfe der Registerkarte „Überprüfung“ erkennen. Wenn ein Thread blockiert ist und auf eine Sperre wartet, wird er in der Thread StateSpalte „ “ mit dem Wert „ BLOCKED“ angezeigt.

Hier ist JConsoleein Beispiel :

Name: Thread-1
State: BLOCKED on Lock 1
  1. VisualVM

VisualVMist ein weiteres Tool, mit dem Sie JavaAnwendungen . Wie JConsolebei können Sie VisualVMDeadlocks erkennen, indem Sie die Registerkarte „Threads“ überprüfen. Wenn ein Thread blockiert ist und auf eine Sperre wartet, wird er in der StateSpalte „ “ mit dem Wert „ BLOCKED“ angezeigt.

Hier ist VisualVMein Beispiel :

Name: Thread-1
State: BLOCKED on Lock 1 owned by Thread-2
  1. LockSupport

LockSupportDie 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 LockSupportzur Erkennung potenzieller Deadlocks:

Thread t = Thread.currentThread();
LockSupport.parkNanos(1000000000);
if (t.getState() == Thread.State.BLOCKED) {
   // Potential deadlock
}

Best Practices zur Vermeidung von Deadlocks

  1. 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();
    }
}

lock1In 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.

  1. 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 DeadlockExampleKlasse 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.

  1. 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 method1und werden Blöcke zum Erlangen bzw. Sperren method2von Sperren synchronizedverwendet . 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.lock1lock2mainlock1method2synchronized

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.

おすすめ

転載: blog.csdn.net/robinhunan/article/details/130614172