天天看點

Java中該如何排查并且預防死鎖呢?

作者:JAVA旭陽

前言

死鎖是并發程式設計中的常見問題,它發生在兩個或多個線程被阻塞,等待對方釋放鎖時。死鎖可能導緻整個系統當機或崩潰,是一個難以複現和修複的問題。在本文中,我們将探讨 Java 中死鎖的成因、檢測方法以及避免死鎖的最佳實踐。

什麼是死鎖?

Java中的死鎖是當兩個或多個線程被阻塞并等待對方釋放資源,這種情況叫做死鎖。換句話說,兩個或多個線程被卡住而無法繼續,因為每個線程都持有另一個線程所需的資源,進而導緻循環依賴。這可能會導緻系統完全當機或崩潰。

例如,考慮兩個線程,線程 A 和線程 B,以及兩個鎖,鎖 1 和鎖 2。線程 A 擷取鎖 1,線程 B 擷取鎖 2。但是,線程 A 需要鎖 2 才能繼續,而線程 B 需要 鎖 1 才能繼續執行,該鎖正被線程 A 持有。這導緻循環依賴,兩個線程都被阻塞并等待另一個線程釋放鎖。這種情況稱為死鎖。

我們直接看一個代碼:

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

在這個例子中,我們有兩個線程,threadA 和 threadB,它們都通路兩個鎖,lock1 和 lock2。threadA先獲得lock1,然後threadB獲得lock2,兩個線程都休眠一秒。然後threadA嘗試擷取threadB持有的lock2,threadB嘗試擷取threadA持有的lock1。這導緻循環依賴,兩個線程都被阻塞并等待另一個線程釋放鎖,進而導緻死鎖。

為了避免這樣的死鎖,您可以遵循并發程式設計的最佳實踐,例如以固定順序擷取鎖、在擷取鎖時使用逾時、最小化鎖的範圍以及使用juc包中的ReentrantLock。

如何在 Java 中檢測死鎖?

檢測死鎖可能是一項具有挑戰性的任務,因為系統似乎已當機或無響應,而且不清楚問題出在哪裡。幸運的是,Java 提供了内置工具來檢測和診斷死鎖。

  1. dump線程資訊

線程dump分析可用于檢測 Java 中的死鎖。線程轉儲是在特定時間點在 Java 虛拟機 (JVM) 中運作的所有線程的狀态快照。通過分析線程轉儲,您可以檢測是否發生了死鎖。線上程轉儲中,您可以查找因等待鎖而被阻塞的線程,并确定哪些鎖由哪些線程持有。如果您在鎖定順序中看到循環依賴,這是潛在死鎖的迹象。

下面是一個顯示潛在死鎖的線程轉儲示例:

"Thread 1" - waiting to lock monitor on Lock 1
"Thread 2" - waiting to lock monitor on Lock 2

Found 1 deadlock.           
  1. JConsole

JConsole 是一個 Java 管理擴充 (JMX) 用戶端,允許您監視和管理 Java 應用程式。您可以使用 JConsole 通過檢查 Threads 頁籤來檢測死鎖。如果有線程被阻塞并等待鎖,它會顯示在“Thread State”列中,值為“BLOCKED”。

下面是顯示阻塞線程的 JConsole 示例:

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

VisualVM 是另一個允許您監視和管理 Java 應用程式的工具。與 JConsole 一樣,您可以使用 VisualVM 通過檢查線程頁籤來檢測死鎖。如果有線程被阻塞并等待鎖,它會顯示在“State”列中,值為“BLOCKED”。

下面是顯示阻塞線程的 VisualVM 示例:

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

LockSupport 類提供一組可用于檢測死鎖的靜态方法。其中一個方法是 parkNanos(),它可用于檢查線程是否被阻塞并等待鎖。如果 parkNanos() 傳回 true,則意味着線程被阻塞,并且存在潛在的死鎖。

下面是使用 LockSupport 檢測潛在死鎖的示例:

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

避免死鎖的最佳實踐

  1. 以固定順序擷取鎖

為避免循環依賴鍊,您應該以固定順序擷取鎖。這意味着如果兩個或多個線程需要擷取多個鎖,它們應該總是以相同的順序擷取它們。例如,如果線程 A 擷取鎖 X,然後擷取鎖 Y,則線程 B 應該先擷取鎖 X,然後再嘗試擷取鎖 Y。

下面是一個以固定順序擷取鎖以避免循環依賴的示例代碼:

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 和 lock2)。兩種方法擷取鎖的順序相同:首先是 lock1,然後是 lock2。這確定了鎖之間沒有循環依賴。

  1. 擷取鎖時使用逾時

為避免死鎖,您可以在擷取鎖時使用逾時。這意味着如果在指定時間内無法擷取鎖,線程将釋放鎖并稍後重試。

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

在這個例子中,我們建立了一個 DeadlockExample 類的執行個體,并啟動了兩個線程,一個運作 method1(),另一個運作 method2()。每個方法都嘗試以不同的順序擷取兩個鎖,這應該可以防止發生任何死鎖。

  1. 最小化鎖的範圍

為避免死鎖,您應該盡量減少鎖的範圍。這意味着您應該隻在必要時擷取鎖并盡快釋放它。這可以通過使用同步塊而不是同步方法來實作。同步塊允許您明确指定鎖的範圍。

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

在method1和method2中,synchronized塊分别用于擷取lock1和lock2上的鎖。在main方法中,建立了兩個線程來調用這兩個方法。當線程開始運作時,一個線程将擷取 lock1 上的鎖,另一個線程将等待直到鎖被釋放。一旦鎖被釋放,等待線程就會擷取到鎖,繼續執行method2内部的synchronized塊。

總結

死鎖是并發程式設計中的常見問題,可能導緻系統完全當機或崩潰。檢測和修複死鎖可能是一項具有挑戰性的任務,但 Java 提供了内置工具來檢測和診斷死鎖。為避免死鎖,您應該以固定順序擷取鎖,在擷取鎖時使用逾時,最小化鎖的範圍。通過遵循這些最佳實踐,您可以降低死鎖的風險并確定您的并發程式順利運作。

繼續閱讀