返回

Java多线程基础(7):死锁的本质与预防

后端

在多线程编程中,死锁是一种令人头疼的难题,它会让线程无限期地等待下去,从而导致程序无法继续运行。要理解死锁,我们首先需要了解可重入锁的概念。

可重入锁

可重入锁是一种特殊类型的锁,它允许同一个线程多次获取同一个锁。也就是说,当一个线程已经持有锁时,它可以再次获取这个锁,而不会被阻塞。这与不可重入锁形成鲜明对比,后者不允许同一个线程多次获取同一个锁。

在多线程编程中,可重入锁通常用于保护共享资源,确保同一时间只有一个线程可以访问这些资源。例如,一个银行账户对象可能会使用一个可重入锁来保护其余额,防止多个线程同时修改余额。

死锁

死锁发生在多个线程相互等待的情况下,每个线程都持有另一个线程需要的锁。这会导致线程陷入无限等待,无法继续执行。

一个常见的死锁示例是两个线程试图获取两个不同的锁。第一个线程首先获取锁A,然后尝试获取锁B。与此同时,第二个线程首先获取锁B,然后尝试获取锁A。这样,两个线程就陷入了死锁,因为每个线程都等待另一个线程释放它所持有的锁。

预防死锁

预防死锁至关重要,因为它会导致程序崩溃。以下是一些常见的预防死锁的方法:

  • 避免嵌套锁 :嵌套锁会导致死锁,因为一个线程可能会在持有外部锁时尝试获取内部锁。
  • 使用定时锁 :为锁设置一个超时时间,如果线程在超时时间内无法获取锁,则释放锁。
  • 使用死锁检测和恢复机制 :在应用程序中实现一种机制,可以检测并恢复死锁。

Java中的死锁检测和恢复

Java中提供了java.util.concurrent.locks.ReentrantLock 类,它实现了可重入锁。ReentrantLock 提供了以下方法来检测和恢复死锁:

  • isLocked() :检查锁是否已被锁定。
  • isHeldByCurrentThread() :检查锁是否由当前线程持有。
  • getHoldCount() :返回当前线程持有的锁的重入次数。
  • lockInterruptibly() :尝试获取锁,如果无法获取则抛出InterruptedException 异常。

使用这些方法,我们可以实现一种死锁检测和恢复机制,如下所示:

public class DeadlockDetector implements Runnable {

    private ReentrantLock lock1;
    private ReentrantLock lock2;

    public DeadlockDetector(ReentrantLock lock1, ReentrantLock lock2) {
        this.lock1 = lock1;
        this.lock2 = lock2;
    }

    @Override
    public void run() {
        try {
            // 尝试获取锁1,如果无法获取则抛出InterruptedException异常
            lock1.lockInterruptibly();

            // 尝试获取锁2,如果无法获取则抛出InterruptedException异常
            lock2.lockInterruptibly();
        } catch (InterruptedException e) {
            // 检测到死锁,恢复程序
            System.out.println("检测到死锁,恢复程序...");
        } finally {
            // 释放锁
            lock1.unlock();
            lock2.unlock();
        }
    }
}

当检测到死锁时,我们可以通过以下方式恢复程序:

  • 中断死锁线程 :使用Thread.interrupt() 方法中断死锁线程,使其抛出InterruptedException 异常。
  • 释放死锁锁 :使用ReentrantLock.unlock() 方法释放死锁线程持有的锁。
  • 重新获取锁 :死锁线程恢复后,可以重新获取锁并继续执行。

通过使用死锁检测和恢复机制,我们可以有效地预防死锁,确保程序的稳定运行。