Java 线程死锁及如何避免死锁介绍

1. 什么是线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去,如下图所示

在上图中,线程 A 已经持有了资源 2,它同时还想申请资源 1,线程 B 已经持有了资源 1,它同时还想申请资源 2,所以线程 1 和线程 2 就因为相互等待对方已经持有的资源,而进入了死锁状态。

2. 死锁产生的原因
那么为什么会产生死锁呢? 主要是由以下四个条件造成的:

(1)互斥条件:系统要求对所分配的资源进行排他性控制,即在一段时间内某个资源仅为一个进程所占有(比如:打印机,同一时间只能一个人打印)。此时若有其他进程请求该资源,则请求只能等待,直到有资源释放了位置;

(2)请求和保持条件:进程已经持有了一个资源,但是又要访问一个新的被其他进程占用的资源那么就会阻塞,并且对自己占用的一个资源保持不放;

(3)不剥夺条件:进程对已经获取的资源未使用完之前不能被剥夺,只能使用完之后自己释放。

(4)环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

下面用一个代码来说明线程死锁:

public class ThreadDeadLock {
    public static void main(String[] args) {
        Object lockA = new Object();
        Object lockB = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockA) {
                    System.out.println("线程1 获得锁A");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程1 等待锁B");
                    synchronized (lockB) {
                        System.out.println("线程1 获得锁B");
                    }
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockB) {
                    System.out.println("线程2 获得锁B");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程2 等待锁A");
                    synchronized (lockA) {
                        System.out.println("线程2 获得锁A");
                    }
                }
            }
        });
        t2.start();
    }
}

输出结果如下:

分析代码结果:Thread-0 是线程1 ,Thread-1 是线程 2,代码首先创建了两个资源,并创建了两个线程。从结果输出可以看出,线程调度器先调度了线程 1,也就是把 CPU 资源分配给了线程 A,线程 A 使用 synchronized(lockA) 方法获得到了 lockA 的监视器锁,然后调用 sleep 函数休眠 1s,休眠 1s 是为了保证线程 1 在获取 lockB 对应的锁前让 线程 2 抢占到 CPU,获取到资源 lockB 对象的监视器锁资源,然后调用 sleep 函数休眠 1s。

好了,到了这里线程 1 获取到了 lockA 资源,线程 2 获取到了 lockB 资源。线程 1 休眠结束后会企图获取 lockB 资源,而 lockB 资源被线程 2 所持有,所以线程 1 会阻塞而等待。而同时线程 2 休眠结束后会企图获取 lockA 资源,而 lockA 资源已经被线程 1 所持有,**所以线程 1 和线程 2 就陷入了相互等待的状态,也就产生了死锁.

首先,lockA 和 lockB 都是互斥资源,当线程 1 调用了 synchronized(lockA) 方法获得到 lockA 上的监视器并释放前,线程 2 再调用 synchronized(lockA) 方法尝试获取该资源会被阻塞,只有线程 1 主动释放该锁,线程 2 才能获得,这满足了资源互斥条件。

线程 1 首先通过 synchronized(lockA) 方法获取到 lockA 上的监视器锁资源,然后通过 synchronized(lockB) 方法等待获取 lockB 上的监视器锁资源,这就构成了请求并持有条件。

线程 1 在获取 lockA 上的监视器锁资源后,该线程不会被线程 2 掠夺走,只有线程 1 自己主动释放 lockA 资源时,他才会放弃对该资源的持有权,这构成了资源不可剥夺条件。

线程 1 持有 lockA 资源并等待获取 lockB 资源,而线程 2 只有 lockB 资源并等待获取 lockA 资源,这构成了环路等待条件。所以线程 1 和线程 2 就进入了死锁状态。
 

3. 如何避免死锁

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,而在操作系统中,互斥条件和不可剥夺条件是系统规定的,这也没办法人为更改,而且这两个条件很明显是一个标准的程序应该所具备的特性。所以目前只有请求并持有和环路等待条件是可以被破坏的。

(1)保持加锁顺序:当多个线程都需要加相同的几个锁的时候(例如上述情况一的死锁),按照不同的顺序枷锁那么就可能导致死锁产生,所以我们如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。

(2)获取锁添加时限:上述死锁代码情况二就是因为出现了获取锁失败无限等待的情况,如果我们在获取锁的时候进行限时等待,例如wait(1000)或者使用ReentrantLock的tryLock(1,TimeUntil.SECONDS)这样在指定时间内获取锁失败就不等待;

(3)进行死锁检测:我们可以通过一些手段检查代码并预防其出现死锁。

造成死锁的原因其实和申请资源的顺序有很大关系 使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程 2 的代码进行如下修改。

// 创建线程 2
 Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockA) {
                    System.out.println("线程2 获得锁A");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程2 等待锁B");
                    synchronized (lockB) {
                        System.out.println("线程2 获得锁B");
                    }
                }
            }
        });
        t2.start();
    }

输出结果如下:

以上的做法属于第一种做法,

如上代码让在线程 2 中获取资源的顺序和在线程 1 中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程 1 和线程 2 都需要资源 1, 2, 3, … . , ,对资源进行排序,线程 1 和线程 2 有在获取了资源 n-1 时才能去获取资源 n。

我们可以简单分析一下为何资源的有序分配会避免死锁,比如上面的代码,假如线程1 和线程 2 同时执行到了 synchronized(lockA),只有1个线程可以获取到 lockA 上的监视器锁,假如线程 1 获取到了,那么线程 2 就会被阻塞而不会再去获取资源 B,线程 1 获取 lockA 的监视器锁后会去申请 lockB 的监视器锁资源,这时候线程 1 是可以获取到的,线程 1 获取到 lockB 资源并使用后会放弃对资源 lockB 的持有,然后再释放对 lockA 的持有,释放 lockA 后线程 2 才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。
 

4.死锁的检测

Java中死锁检测手段最多的就是使用JDK带有的jstack和JConsole工具了。下面我们以jstack为例来进行死锁的检测;

(1)先运行我们的代码程序

(2)使用JDK的工具JPS查看运行的进程信息,如下:

 (3)使用jps查看到的进程ID对其进行jstack 进程分析

D:\Views\BlogRecyclerViewSec\KotilnTest>jstack 4184
2022-10-31 17:12:29
Full thread dump OpenJDK 64-Bit Server VM (25.242-b01 mixed mode):

"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x00000000010dd800 nid=0x22d0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001e88a800 nid=0x16f8 waiting for monitor entry [0x000000002050f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.example.lib.ThreadDeadLock$2.run(ThreadDeadLock.java:37)
        - waiting to lock <0x000000076b629420> (a java.lang.Object)
        - locked <0x000000076b629430> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001e887800 nid=0x3658 waiting for monitor entry [0x000000002040f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.example.lib.ThreadDeadLock$1.run(ThreadDeadLock.java:19)
        - waiting to lock <0x000000076b629430> (a java.lang.Object)
        - locked <0x000000076b629420> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x000000001e851000 nid=0x3408 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #9 daemon prio=9 os_prio=2 tid=0x000000001e7c3800 nid=0x3a5c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #8 daemon prio=9 os_prio=2 tid=0x000000001e7c2800 nid=0x1300 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #7 daemon prio=9 os_prio=2 tid=0x000000001e7ba800 nid=0x3084 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #6 daemon prio=9 os_prio=2 tid=0x000000001e7b4000 nid=0x3074 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001e7ae000 nid=0x3b8c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001e75b000 nid=0x2878 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001d062000 nid=0x258c in Object.wait() [0x000000001faae000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b588ee8> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x000000076b588ee8> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001e743000 nid=0x1170 in Object.wait() [0x000000001f9ae000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b586c08> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x000000076b586c08> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=2 tid=0x000000001d054800 nid=0x3f6c runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000003337000 nid=0x1538 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000003338800 nid=0x32d8 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000000000333a800 nid=0x2424 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000000000333c000 nid=0x3cdc runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x000000000333e000 nid=0x335c runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x000000000333f800 nid=0x362c runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000003342800 nid=0x17dc runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000003343800 nid=0x2de8 runnable

"GC task thread#8 (ParallelGC)" os_prio=0 tid=0x0000000003346000 nid=0x2628 runnable

"GC task thread#9 (ParallelGC)" os_prio=0 tid=0x0000000003347000 nid=0x30b4 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000000001e80d800 nid=0x2108 waiting on condition

JNI global references: 5


Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001d061898 (object 0x000000076b629420, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001d05f0b8 (object 0x000000076b629430, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.example.lib.ThreadDeadLock$2.run(ThreadDeadLock.java:37)
        - waiting to lock <0x000000076b629420> (a java.lang.Object)
        - locked <0x000000076b629430> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at com.example.lib.ThreadDeadLock$1.run(ThreadDeadLock.java:19)
        - waiting to lock <0x000000076b629430> (a java.lang.Object)
        - locked <0x000000076b629420> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

分析的结果很长,我们往下找可以看到“Found one Java-level deadlock”,表示程序中发现了一个死锁;

jstack 可参考jstack 工具 查看JVM堆栈信息_大渔歌_的博客-CSDN博客_jstack 打印堆栈信息

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值