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

在这个例子中,我们有两个线程,threadAthreadB,它们都访问两个锁,lock1lock2threadA先获得lock1,然后threadB获得lock2,两个线程都休眠一秒。然后threadA尝试获取threadB持有的lock2threadB尝试获取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();
    }
}

在这个例子中,我们有两个线程,每个线程调用一个方法,以固定顺序获取两个锁(lock1lock2)。两种方法获取锁的顺序相同:首先是 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();
    }
}

method1method2中,synchronized块分别用于获取lock1lock2上的锁。在main方法中,创建了两个线程来调用这两个方法。当线程开始运行时,一个线程将获取 lock1 上的锁,另一个线程将等待直到锁被释放。一旦锁被释放,等待线程就会获取到锁,继续执行method2内部的synchronized块。

总结

死锁是并发编程中的常见问题,可能导致系统完全冻结或崩溃。检测和修复死锁可能是一项具有挑战性的任务,但 Java 提供了内置工具来检测和诊断死锁。为避免死锁,您应该以固定顺序获取锁,在获取锁时使用超时,最小化锁的范围。通过遵循这些最佳实践,您可以降低死锁的风险并确保您的并发程序顺利运行。

猜你喜欢

转载自blog.csdn.net/robinhunan/article/details/130614172