Java 多线程死锁及示例

2025年4月8日 | 阅读 8 分钟

Java 中的死锁是多线程的一部分。当一个线程等待另一个线程获得的某个对象的锁,而第二个线程又等待第一个线程获得的某个对象的锁时,就可能发生死锁。由于两个线程都在等待对方释放锁,这种情况就称为死锁。

Deadlock in Java

死锁的条件

  1. 互斥:至少有一个资源必须被以不可共享的方式持有。换句话说,同一时间只有一个进程可以使用该资源。
  2. 持有并等待:一个进程持有至少一个资源,并等待获取当前被其他进程持有的额外资源。
  3. 非抢占:资源不能被强制从持有它们的进程中移除。进程必须自愿释放资源。
  4. 循环等待:一组进程在循环链中相互等待,其中每个进程都持有一个资源,并等待链中的下一个进程释放另一个资源。

需要注意的是,即使上述条件中的任何一个不满足,也不会发生死锁。

Java 死锁示例

示例

编译并运行

输出

Thread 1: locked resource 1
Thread 2: locked resource 2

说明

有两个资源:resource1 和 resource2。第一个线程 t1 锁定 resource1 并休眠。同样,第二个线程 t2 锁定 resource2 并休眠。每个线程休眠 100 毫秒。

之后,第一个线程 t1 尝试获取 resource2,第二个线程 t2 尝试获取 resource1。然而,resource1 已被线程 t1 获取,resource2 已被线程 t2 获取,导致死锁情况,输出也证实了这一点。第二和第四个打印语句没有执行。

更复杂的死锁

死锁可能还包括两个以上的线程。原因是检测死锁可能很困难。下面是一个四个线程发生死锁的示例

线程 1 锁定 A,等待 B

线程 2 锁定 B,等待 C

线程 3 锁定 C,等待 D

线程 4 锁定 D,等待 A

线程 1 等待线程 2,线程 2 等待线程 3,线程 3 等待线程 4,线程 4 等待线程 1。

死锁检测

观察以下步骤来检测死锁。

步骤 1:分别使用命令 javacJava 编译并执行程序。

这里,我们执行上面编写的包含死锁的程序。

Deadlock in Java

步骤 2:打开另一个命令提示符窗口,运行命令 jps -l

命令 jps -l 列出所有正在运行的 Java 进程(带进程 ID)。

Deadlock in Java

我们可以看到 Main 的进程 ID 是 8680。

步骤 3:在命令提示符中键入以下命令并运行

将 <PID> 替换为进程 ID 8680 并观察输出。

Deadlock in Java

输出

8680:
2025-04-03 13:46:30
Full thread dump Java HotSpot(TM) 64-Bit Server VM (12.0.2+10 mixed mode, sharing):

Threads class SMR info:
_java_thread_list=0x000000ddaf977c70, length=12, elements={
0x000000ddae6d0000, 0x000000ddae6d1000, 0x000000ddae6e9800, 0x000000ddae6f0800,
0x000000ddae6f1800, 0x000000ddae6f5800, 0x000000ddae6f8800, 0x000000ddaf911000,
0x000000ddaf95b000, 0x000000ddaf976000, 0x000000ddaf976800, 0x000000dd8f70f800
}
"Reference Handler" #2 daemon prio=10 os_prio=2 cpu=0.00ms elapsed=448.77s tid=0x000000ddae6d0000 nid=0x5e68 waiting on condition  [0x000000ddaf14f000]
   java.lang.Thread.State: RUNNABLE
        at java.lang.ref.Reference.waitForReferencePendingList(java.base@12.0.2/Native Method)
        at java.lang.ref.Reference.processPendingReferences(java.base@12.0.2/Reference.java:241)
        at java.lang.ref.Reference$ReferenceHandler.run(java.base@12.0.2/Reference.java:213)

"Finalizer" #3 daemon prio=8 os_prio=1 cpu=0.00ms elapsed=448.77s tid=0x000000ddae6d1000 nid=0x29ac in Object.wait()  [0x000000ddaf24e000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(java.base@12.0.2/Native Method)
        - waiting on <0x000000008a90af78> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(java.base@12.0.2/ReferenceQueue.java:155)
        - locked <0x000000008a90af78> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(java.base@12.0.2/ReferenceQueue.java:176)
        at java.lang.ref.Finalizer$FinalizerThread.run(java.base@12.0.2/Finalizer.java:170)

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=448.77s tid=0x000000ddae6e9800 nid=0x5220 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Attach Listener" #5 daemon prio=5 os_prio=2 cpu=0.00ms elapsed=448.77s tid=0x000000ddae6f0800 nid=0x52d8 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #6 daemon prio=9 os_prio=2 cpu=15.63ms elapsed=448.77s tid=0x000000ddae6f1800 nid=0x1c4 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

"C1 CompilerThread0" #8 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=448.77s tid=0x000000ddae6f5800 nid=0x5338 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

"Sweeper thread" #9 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=448.77s tid=0x000000ddae6f8800 nid=0x5e70 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Service Thread" #10 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=448.76s tid=0x000000ddaf911000 nid=0x3040 runnable  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Common-Cleaner" #11 daemon prio=8 os_prio=1 cpu=0.00ms elapsed=448.75s tid=0x000000ddaf95b000 nid=0x888 in Object.wait()  [0x000000ddaff2e000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
        at java.lang.Object.wait(java.base@12.0.2/Native Method)
        - waiting on <0x000000008a9e7440> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(java.base@12.0.2/ReferenceQueue.java:155)
        - locked <0x000000008a9e7440> (a java.lang.ref.ReferenceQueue$Lock)
        at jdk.internal.ref.CleanerImpl.run(java.base@12.0.2/CleanerImpl.java:148)
        at java.lang.Thread.run(java.base@12.0.2/Thread.java:835)
        at jdk.internal.misc.InnocuousThread.run(java.base@12.0.2/InnocuousThread.java:134)

"Thread-0" #12 prio=5 os_prio=0 cpu=0.00ms elapsed=448.75s tid=0x000000ddaf976000 nid=0x2100 waiting for monitor entry  [0x000000ddb002f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at Main$1.run(Main.java:15)
        - waiting to lock <0x000000008a800800> (a java.lang.String)
        - locked <0x000000008a800000> (a java.lang.String)

"Thread-1" #13 prio=5 os_prio=0 cpu=0.00ms elapsed=448.75s tid=0x000000ddaf976800 nid=0x593c waiting for monitor entry  [0x000000ddb012f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at Main$2.run(Main.java:30)
        - waiting to lock <0x000000008a800000> (a java.lang.String)
        - locked <0x000000008a800800> (a java.lang.String)

"DestroyJavaVM" #14 prio=5 os_prio=0 cpu=31.25ms elapsed=448.75s tid=0x000000dd8f70f800 nid=0x5db4 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"VM Thread" os_prio=2 cpu=0.00ms elapsed=448.78s tid=0x000000ddae6c9000 nid=0x5da4 runnable

"GC Thread#0" os_prio=2 cpu=0.00ms elapsed=448.78s tid=0x000000dd8f750000 nid=0x5b84 runnable

"G1 Main Marker" os_prio=2 cpu=0.00ms elapsed=448.78s tid=0x000000dd8f75d800 nid=0x1134 runnable

"G1 Conc#0" os_prio=2 cpu=0.00ms elapsed=448.78s tid=0x000000dd8f760000 nid=0x3624 runnable

"G1 Refine#0" os_prio=2 cpu=0.00ms elapsed=448.78s tid=0x000000dd8f7fa800 nid=0x56b8 runnable

"G1 Young RemSet Sampling" os_prio=2 cpu=0.00ms elapsed=448.78s tid=0x000000dd8f7fb800 nid=0x2464 runnable
"VM Periodic Task Thread" os_prio=2 cpu=0.00ms elapsed=448.75s tid=0x000000ddaf959800 nid=0x19bc waiting on condition

JNI global refs: 4, weak refs: 0


Found one Java-level deadlock:
=============================
"Thread-0":
  waiting to lock monitor 0x000000ddae6db280 (object 0x000000008a800800, a java.lang.String),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 0x000000ddae6d9280 (object 0x000000008a800000, a java.lang.String),
  which is held by "Thread-0"

Java stack information for the threads listed above:
===================================================
"Thread-0":
        at Main$1.run(Main.java:15)
        - waiting to lock <0x000000008a800800> (a java.lang.String)
        - locked <0x000000008a800000> (a java.lang.String)
"Thread-1":
        at Main$2.run(Main.java:30)
        - waiting to lock <0x000000008a800000> (a java.lang.String)
        - locked <0x000000008a800800> (a java.lang.String)

Found 1 deadlock.

我们可以看到输出显示找到 1 个死锁

防止死锁

问题的解决方案在于其根源。在死锁中,访问资源 A 和 B 的模式是主要问题。要解决该问题,我们只需重新排序访问共享资源的语句即可。

示例

编译并运行

输出

In block 1
In block 2

说明

在上面的程序中,有两个线程 t1 和 t2。在生成两个线程后,线程 t1 获取资源 b 并休眠 100 毫秒。在此期间,线程 t2 也尝试获取资源 b,由于它已被线程 t1 获取,因此线程 t2 等待。

之后,线程 t1 从休眠中唤醒,对资源 a 应用锁定,然后完成其执行。从而释放资源 a 和 b。线程 t2 现在获取资源 b,然后获取资源 a 并执行其任务。请注意,两个线程都完成了它们的工作。因此,没有发生死锁。

如何在 Java 中避免死锁?

死锁无法完全解决。但是,我们可以通过遵循以下基本规则来避免它们:

  1. 避免嵌套锁:我们必须避免为多个线程授予锁;这是死锁条件的主要原因。这通常发生在为您提供多个线程的锁时。
  2. 避免不必要的锁:应将锁授予重要的线程。将锁授予导致死锁条件的不必要线程。
  3. 使用 Thread Join:当一个线程等待另一个线程完成时,通常会发生死锁。在这种情况下,我们可以使用 join 并设置线程将花费的最大时间。

要阅读更多点击这里

要记住的重要事项

  1. 如果多个线程使用同步块或方法访问共享资源,它们可能会导致死锁情况。
  2. 不当使用锁可能导致死锁。
  3. 尽量减少嵌套同步块的使用。这会降低发生死锁的可能性。
  4. 使用带超时的锁以避免无限等待。
  5. VisualVM 或 jConsole 等工具可以在运行时帮助检测死锁。
  6. 死锁无法自动解决。开发人员需要避免导致死锁的情况,或者使用锁层次结构等技术来确保线程按特定顺序获取锁。

Java 死锁选择题

1. Java 中的死锁是什么?

  1. 当线程顺序执行时
  2. 当线程并行执行时
  3. 当线程突然终止时
  4. 当线程永远处于等待状态并且无法继续进行时
 

答案 4

解释:当一个线程等待另一个线程获得的某个对象的锁,而第二个线程又等待第一个线程获得的某个对象的锁时,就可能发生死锁。由于两个线程都在等待对方释放锁,这种情况就称为死锁。


2. 下列哪项不是发生死锁的条件?

  1. 循环等待
  2. 互斥
  3. 抢占
  4. 持有并等待
 

答案 3

解释:抢占不是死锁的条件,因为资源不能被强制从持有它们的进程中移除。进程必须自愿释放资源。


3. 防止死锁的最佳方法是什么?

  1. 在没有限制的情况下使用同步方法
  2. 避免嵌套锁并以一致的顺序获取锁
  3. 让 JVM 自动处理
  4. 使用无限循环重试锁获取
 

答案 2

解释:死锁无法自动解决。开发人员需要避免导致死锁的情况,或者使用锁层次结构等技术来确保线程按特定顺序获取锁。


4. 避免 Java 中死锁的常用方法是什么?

  1. 为获取锁使用超时
  2. 让线程竞争锁
  3. 随机分配锁
  4. 根本不使用锁
 

答案 1

解释:使用带超时的锁以避免无限等待。


5. Java 开发工具包 (JDK) 中的哪个工具可以帮助诊断死锁?

  1. javac
  2. jstack
  3. javadoc
  4. jar
 

答案 2

解释:用于诊断死锁的主要 JDK 工具是 jstack,它可以用来生成线程转储,显示所有线程的状态,包括参与死锁的线程,并确定它们正在等待哪些锁。