赞
踩
死锁是并发编程中的常见问题,它发生在两个或多个线程被阻塞,等待对方释放锁时。死锁可能导致整个系统冻结或崩溃,是一个难以复现和修复的问题。在本文中,我们将探讨 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 提供了内置工具来检测和诊断死锁。
线程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.
JConsole
是一个 Java 管理扩展 (JMX) 客户端,允许您监视和管理 Java 应用程序。您可以使用 JConsole
通过检查 Threads
选项卡来检测死锁。如果有线程被阻塞并等待锁,它会显示在“Thread State
”列中,值为“BLOCKED
”。
下面是显示阻塞线程的 JConsole
示例:
Name: Thread-1
State: BLOCKED on Lock 1
VisualVM
是另一个允许您监视和管理 Java
应用程序的工具。与 JConsole
一样,您可以使用 VisualVM
通过检查线程选项卡来检测死锁。如果有线程被阻塞并等待锁,它会显示在“State
”列中,值为“BLOCKED
”。
下面是显示阻塞线程的 VisualVM
示例:
Name: Thread-1
State: BLOCKED on Lock 1 owned by Thread-2
LockSupport
类提供一组可用于检测死锁的静态方法。其中一个方法是parkNanos()
,它可用于检查线程是否被阻塞并等待锁。如果 parkNanos()
返回 true,则意味着线程被阻塞,并且存在潜在的死锁。
下面是使用 LockSupport
检测潜在死锁的示例:
Thread t = Thread.currentThread();
LockSupport.parkNanos(1000000000);
if (t.getState() == Thread.State.BLOCKED) {
// Potential deadlock
}
为避免循环依赖链,您应该以固定顺序获取锁。这意味着如果两个或多个线程需要获取多个锁,它们应该总是以相同的顺序获取它们。例如,如果线程 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
。这确保了锁之间没有循环依赖。
为避免死锁,您可以在获取锁时使用超时。这意味着如果在指定时间内无法获取锁,线程将释放锁并稍后重试。
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()
。每个方法都尝试以不同的顺序获取两个锁,这应该可以防止发生任何死锁。
为避免死锁,您应该尽量减少锁的范围。这意味着您应该只在必要时获取锁并尽快释放它。这可以通过使用同步块而不是同步方法来实现。同步块允许您明确指定锁的范围。
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 提供了内置工具来检测和诊断死锁。为避免死锁,您应该以固定顺序获取锁,在获取锁时使用超时,最小化锁的范围。通过遵循这些最佳实践,您可以降低死锁的风险并确保您的并发程序顺利运行。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。