【Java多线程进阶】死锁

前言

死锁(Deadlock)是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果不提前预防或外界干扰,这些线程将无法执行下去。因此,本篇博文讲解造成死锁的原因以及解决方案。

目录

1. 造成死锁的原因

2. 哲学家就餐问题

2.1 造成死锁的原因

2.2 解决死锁问题


1. 造成死锁的原因

死锁是多个线程在相互等待对方所占有的锁才能继续执行下去,造成线程都被阻塞都无法执行下去,也释放不了线程自己的锁资源,造成僵局。

举例说明:

我和朋友去拉面馆吃面,我先拿起了辣椒他拿起了醋。此时他想要辣椒、我想要醋,但我俩谁也不让谁。

我对他说:你先把辣椒给我,我用完了就把醋给你。他对我说:你先把醋给我,我用完了就把辣椒给你。

我和他也不让谁,最后导致死锁。在上述例子中,辣椒和醋就是两个锁,我和他就是两个线程。关于死锁会引申的一个哲学家就餐问题。


2. 哲学家就餐问题

哲学家就餐问题是一个经典的并发编程问题,它描述的是五个哲学家围坐在一张圆形餐桌旁,每个哲学家面前有一碗饭和一只筷子,碗和筷子都是共享资源。

哲学家需要用一双筷子来吃饭,但是只有相邻的两个哲学家之间有一只筷子,因此他们必须在餐桌上竞争筷子,才能拿到一双筷子吃饭。

这个问题会引发死锁(Deadlock)和饥饿(Starvation)等并发编程中的经典问题。

如果所有哲学家同时都想获取餐具,但每个哲学家只能等待旁边的哲学家放下筷子才能获取餐具,这可能会导致死锁。

如果所有哲学家只有在其他人都吃到饭才能拿到自己的餐具,那么有些哲学家可能会因为等待时间太长而饥饿。

代码案例:

有两个哲学家在进行就餐,哲学家1获取到筷子1后继续尝试获取筷子2,哲学家2获取到筷子2后继续尝试获取筷子1,造成了死锁状态。

public class DeadlockDemo {
        public static void main(String[] args) {
            Object obj1 = new Object();//筷子1
            Object obj2 = new Object();//筷子2

            // 线程1获取obj1,然后等待obj2
            Thread thread1 = new Thread(() -> {
                synchronized (obj1) {
                    System.out.println(Thread.currentThread().getName() + " 获取 obj1锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj2) {
                        System.out.println(Thread.currentThread().getName() + " 获取 obj2锁");
                    }
                }
            }, "哲学家 1");

            // 线程2获取obj2,然后等待obj1
            Thread thread2 = new Thread(() -> {
                synchronized (obj2) {
                    System.out.println(Thread.currentThread().getName() + " 获取 obj2锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (obj1) {
                        System.out.println(Thread.currentThread().getName() + " 获取 obj1锁");
                    }
                }
            }, "哲学家 2");

            t1.start();
            t2.start();
        }
}

运行后打印:

上述代码,中 thread1 是哲学家1,thread2 是哲学家2,obj1 锁和 obj2 锁分别代表两只筷子。哲学家1 占用了 obj1 锁,哲学家2 占用了 obj2 锁。

此时 哲学家1 在拥有 obj1 时获取 obj2 是获取不到的,哲学家2 在拥有 obj2 时获取 obj1 也是获取不到的。

这样就会造成死锁。也就是本篇文章开头所说的,一个线程处于永久获取到锁的状态,其他线程无限阻塞等待,这就造成了死锁。


2.1 造成死锁的原因

  • 互斥:一个资源只能被一个进程独占,即该资源只能被一个进程占有或使用。
  • 请求和保持:进程已经保持了至少一个资源,并还在尝试获取其他进程保持的资源。
  • 不可抢占:某个资源只能由占有它的线程释放,不能被其他线程强制占有或抢占。
  • 循环等待:假设一个队列有{t1,t2,t3}这三个线程,t1 等待 t2 所占资源,t2 等待 t3 所占资源,t3 等待 t1 所占资源,这样就会造成循环等待。 

2.2 解决死锁问题

解决死锁问题,最好的方式就是预防死锁。我们把锁按照顺序进行编号,并且约定每个线程在获取多把锁的时候必须先获取编号小的,后获取编号大的。这样就不会形成死锁了。

因为,最先获取编号小锁的线程会优先完成相应任务。轮到其他线程再获取小编号锁时,锁就处于空闲状态了。

代码案例:

public class DeadlockDemo {
        public static void main(String[] args) {
            Object obj1 = new Object();//筷子1
            Object obj2 = new Object();//筷子2

            
            Thread t1 = new Thread(() -> {
                synchronized (obj1) {
                    System.out.println(Thread.currentThread().getName() + " 获取 obj1锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj2) {
                        System.out.println(Thread.currentThread().getName() + " 获取 obj2锁");
                    }
                }
            }, "哲学家 1");

            
            Thread t2 = new Thread(() -> {
                synchronized (obj1) {
                    System.out.println(Thread.currentThread().getName() + " 获取 obj2锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (obj2) {
                        System.out.println(Thread.currentThread().getName() + " 获取 obj1锁");
                    }
                }
            }, "哲学家 2");

            t1.start();
            t2.start();
        }
}

运行后打印:

在上述代码中,我们创建了两个锁对象 obj1 和 obj2 分别代表两只筷子,并创建两个线程 thread1 和 thread 2 分别代表两个哲学家。

当 哲学家1 首先获取到 obj1 后,由于 obj2 并未线程占用,则立马获取 obj2 ,获得了两双筷子,就餐后释放 obj1 和 obj2。

这时 哲学家2 尝试获取 obj2 和 obj1 时,发现两双筷子都没人使用,直接开始就餐。


什么是死锁?

当多个线程并发后,线程在占有锁资源的情况下还尝试获取别的线程所占有的锁资源。这样就会操作线程永远获取不到其他锁资源,也不能释放本身锁资源。从而形成的一种僵局,这就是死锁。

造成死锁的原因是什么?

造成死锁的原因有,互斥、请求和保持、不可抢占、循环等待。(文章中有讲解)

死锁的解决方案

我们可以对多个锁按照顺序进行编号,并约定每个线程在使用多把锁时优先使用最小编号的锁,这样就能保证其他线程再获取小编号锁时,锁资源已得到释放。


本篇博文到这里就结束了,感谢点赞、评论、收藏、关注~

猜你喜欢

转载自blog.csdn.net/weixin_64916311/article/details/130962777