聊聊“死锁“

news/2024/5/20 12:48:10 标签: java, 死锁, 操作系统

死锁”或者Deadlock是计算机科学中一个重要的概念,说得是在并发系统中的一种状态,其中多个进程或线程无限期地等待资源,而无法继续执行下去。当发生死锁时,系统中的进程或线程会陷入一种僵持状态,无法继续进行,导致系统无法正常运行。

死锁产生的条件

死锁发生的条件通常包括以下四个必要条件:
1)互斥条件(Mutual Exclusion):至少有一个资源被标记为独占资源,即一次只能被一个进程或线程使用。
2)占有和等待条件(Hold and Wait):进程或线程已经获得了一些资源,并且还在等待获取其他资源,同时保持对已有资源的占有。
3)不可抢占条件(No Preemption):已经分配给进程或线程的资源不能被强制性地剥夺,只能由占有资源的进程或线程主动释放。
4)循环等待条件(Circular Wait):存在一个进程或线程的资源等待链,使得每个进程或线程都在等待下一个资源的释放。
当这四个条件同时满足时,就可能发生死锁。一旦发生死锁,系统无法自动解除,因为每个进程或线程都在等待其他资源的释放,形成了循环等待。

死锁示例

生活中得例子

“哲学家就餐问题”。这个问题描述了五位哲学家围坐在圆桌旁,每位哲学家面前放着一个盘子和一支叉子,哲学家需要交替地使用左右两边的叉子才能进餐。
情景如下:
哲学家A坐在桌子上,需要获取左右两边的叉子才能进餐。
哲学家B坐在桌子上,需要获取左右两边的叉子才能进餐。
哲学家C坐在桌子上,需要获取左右两边的叉子才能进餐。
哲学家D坐在桌子上,需要获取左右两边的叉子才能进餐。
哲学家E坐在桌子上,需要获取左右两边的叉子才能进餐。
假设每位哲学家都首先尝试获取左边的叉子,然后再尝试获取右边的叉子。如果所有哲学家同时开始进餐,并且每个哲学家都先拿起左边的叉子,然后等待右边的叉子,那么这种情况下可能会导致死锁

具体流程如下:
哲学家A尝试获取左边的叉子,但叉子已被哲学家E占有,所以哲学家A等待左边的叉子。
哲学家B尝试获取左边的叉子,但叉子已被哲学家A占有,所以哲学家B等待左边的叉子。
哲学家C尝试获取左边的叉子,但叉子已被哲学家B占有,所以哲学家C等待左边的叉子。
哲学家D尝试获取左边的叉子,但叉子已被哲学家C占有,所以哲学家D等待左边的叉子。
哲学家E尝试获取左边的叉子,但叉子已被哲学家D占有,所以哲学家E等待左边的叉子。
在这个例子中,每个哲学家都占有一个叉子,并等待另一个叉子的释放,但没有哲学家能够继续执行下去,系统陷入了死锁状态。

进程死锁

假设有两个进程A和B,以及两个共享资源X和Y。每个进程需要同时占有两个资源才能完成任务。
情景如下:
进程A获得了资源X,并等待资源Y。
进程B获得了资源Y,并等待资源X。
此时,两个进程都无法继续执行,因为它们都在等待对方占有的资源。这就是一个死锁的状态,两个进程都无法继续执行下去,系统陷入僵持状态。

具体流程如下:
进程A开始执行,并获得资源X。
进程B开始执行,并获得资源Y。
进程A尝试请求资源Y,但此时资源Y已被进程B占有,所以进程A等待资源Y释放。
进程B尝试请求资源X,但此时资源X已被进程A占有,所以进程B等待资源X释放。
在这个例子中,互斥条件满足,因为资源X和Y只能被一个进程占有。占有和等待条件也满足,因为进程A占有资源X并等待资源Y,进程B占有资源Y并等待资源X。不可抢占条件也满足,因为已经分配给进程的资源不能被强制性地剥夺。最后,循环等待条件满足,因为进程A等待进程B占有的资源,进程B等待进程A占有的资源。

线程死锁

当涉及多个线程并发执行时,线程死锁是一个常见的问题。下面是一个简单的Java线程死锁的例子,涉及两个线程和两个共享资源:

java">public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();
    public static void main(String[] args) {
        // 线程1
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1 acquired resource 1");
                try {
                    Thread.sleep(100); // 等待一段时间,给线程2机会获取资源
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource2) {
                    System.out.println("Thread 1 acquired resource 2");
                }
            }
        });
        // 线程2
        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2 acquired resource 2");
                synchronized (resource1) {
                    System.out.println("Thread 2 acquired resource 1");
                }
            }
        });
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

在这个例子中,线程1尝试获取资源1,然后等待一段时间,给线程2机会获取资源2。同时,线程2尝试获取资源2,然后等待线程1释放资源1。由于线程1持有资源1并等待资源2,而线程2持有资源2并等待资源1,两个线程陷入了相互等待的状态,造成死锁

解决死锁问题的方法

1)预防死锁
2)避免死锁
3)检测与恢复。
预防死锁是通过破坏死锁产生的四个条件之一来避免死锁的发生。避免死锁是在资源分配过程中使用算法和策略,动态地避免可能导致死锁的资源分配组合。检测与恢复是在系统中定期检测死锁的存在,并采取恢复措施,如剥夺资源或通过进程终止来解除死锁

解决死锁示例

1.应对“哲学家就餐问题”
资源有序分配:引入一个全局的资源分配策略,确保哲学家按照一定的顺序获取叉子,从而避免循环等待。例如,可以规定哲学家只有在同时获取到左右两个叉子时才能进餐,或者规定奇数号哲学家先拿左边的叉子,偶数号哲学家先拿右边的叉子等。这样可以避免所有哲学家同时竞争同一个叉子,减少死锁的可能性。
资源分配时限:引入一个超时机制,如果一个哲学家等待时间过长仍未获取到所需的叉子,就放弃当前的叉子并重新开始整个获取过程,避免长时间的资源占有。这可以避免一个哲学家长时间等待一个叉子而导致死锁的发生。
可预见性资源分配:通过将资源分配预先安排好,确保每个哲学家在执行过程中能够立即获取到所需的叉子,从而避免竞争和死锁。例如,可以为每个哲学家分配一个专属的叉子,或者为奇数号哲学家分配左边的叉子,偶数号哲学家分配右边的叉子等。
死锁检测与恢复:使用死锁检测算法来监测系统是否陷入死锁状态,一旦检测到死锁,采取相应的措施进行恢复。例如,可以通过中断一个或多个哲学家的进餐过程,释放其占有的叉子,打破死锁的循环等待条件。

2.应多“AB进程抢占XY资源”
资源有序分配:引入一个全局的资源分配策略,确保进程按照一定的顺序获取资源,从而避免循环等待。例如,可以规定进程只有在同时获取到资源X和资源Y时才能执行,或者规定进程A在执行之前必须先获取资源X,进程B在执行之前必须先获取资源Y等。这样可以避免进程之间同时竞争同一个资源,减少死锁的可能性。
资源分配时限:引入一个超时机制,如果一个进程等待时间过长仍未获取到所需的资源,就放弃当前的资源并重新开始整个获取过程,避免长时间的资源占有。这可以避免一个进程长时间等待一个资源而导致死锁的发生。
避免持有多个资源:如果可能,设计系统时尽量避免一个进程同时持有多个资源。如果一个进程只需要其中一个资源,而不需要同时持有两个资源,就能减少死锁的可能性。
死锁检测与恢复:使用死锁检测算法来监测系统是否陷入死锁状态,一旦检测到死锁,采取相应的措施进行恢复。例如,可以通过中断一个或多个进程的执行,释放其占有的资源,打破死锁的循环等待条件。

3.应对Java多线程产生的死锁
Java线程死锁问题,可以采取以下解决办法:
避免嵌套锁定:确保在一个线程持有锁时,不去请求其他的锁。可以通过合理设计代码逻辑和锁的粒度来避免嵌套锁定的情况发生。
锁的顺序获取:约定所有线程获取锁的顺序,按照相同的顺序获取锁可以避免循环等待的情况。例如,对于示例中的资源1和资源2,可以约定所有线程获取资源的顺序是先获取资源1,再获取资源2。
死锁检测与恢复:使用死锁检测算法来监测系统是否陷入死锁状态,一旦检测到死锁,可以通过中断某些线程或回滚操作来恢复系统。Java中的线程管理工具和死锁检测工具可以帮助进行死锁检测和恢复。
例如Java中的Lock接口提供了tryLock()方法,可以尝试获取锁而不进入等待状态。通过使用tryLock()方法,可以在获取锁失败时立即释放已经获取的锁,避免发生死锁

java">import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockExample {
    private static final Lock lock1 = new ReentrantLock();
    private static final Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            boolean locked1 = false;
            boolean locked2 = false;

            try {
                locked1 = lock1.tryLock(); // 尝试获取锁1
                locked2 = lock2.tryLock(); // 尝试获取锁2
            } finally {
                if (locked1 && locked2) {
                    System.out.println("Thread 1 acquired both locks");
                    // 执行相关操作
                    lock2.unlock();
                    lock1.unlock();
                } else if (locked1) {
                    lock1.unlock();
                } else if (locked2) {
                    lock2.unlock();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            boolean locked1 = false;
            boolean locked2 = false;

            try {
                locked2 = lock2.tryLock(); // 尝试获取锁2
                locked1 = lock1.tryLock(); // 尝试获取锁1
            } finally {
                if (locked1 && locked2) {
                    System.out.println("Thread 2 acquired both locks");
                    // 执行相关操作
                    lock1.unlock();
                    lock2.unlock();
                } else if (locked1) {
                    lock1.unlock();
                } else if (locked2) {
                    lock2.unlock();
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

合理的资源管理:在设计多线程应用时,合理管理共享资源是非常重要的。使用并发容器或线程安全的数据结构可以减少对显式锁的需求,从而降低发生死锁的风险。例如下面的例子中使用ConcurrentHashMap:

java">import java.util.concurrent.ConcurrentHashMap;

public class ResourceManagementExample {
    private static final ConcurrentHashMap<Integer, Integer> resources = new ConcurrentHashMap<>();
    public static void main(String[] args) {
        // 初始化资源
        resources.put(1, 1);
        resources.put(2, 2);
        resources.put(3, 3);
        Thread thread1 = new Thread(() -> {
            // 获取资源1
            Integer resource1 = resources.get(1);
            synchronized (resource1) {
                System.out.println("Thread 1 acquired resource 1");
                try {
                    Thread.sleep(100); // 模拟执行某些操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取资源2
                Integer resource2 = resources.get(2);
                synchronized (resource2) {
                    System.out.println("Thread 1 acquired resource 2");
                    // 执行相关操作
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            // 获取资源2
            Integer resource2 = resources.get(2);
            synchronized (resource2) {
                System.out.println("Thread 2 acquired resource 2");
                // 获取资源3
                Integer resource3 = resources.get(3);
                synchronized (resource3) {
                    System.out.println("Thread 2 acquired resource 3");
                    // 执行相关操作
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}


http://www.niftyadmin.cn/n/416648.html

相关文章

C/C++基础讲解(一百零七)之经典篇(几岁/逆序打印/回文)

C/C++基础讲解(一百零七)之经典篇(几岁/逆序打印/回文) 程序之美 前言 很多时候,特别是刚步入大学的学子们,对于刚刚开展的计算机课程基本上是一团迷雾,想要弄明白其中的奥秘,真的要花费一些功夫,我和大家一样都是这么啃过来的,从不知到知知,懵懂到入门,每一步都走的…

培训班出来拿17K,入职后8天就被裁了....

最近翻了一些网站的招聘信息&#xff0c;把一线大厂和大型互联网公司看了个遍&#xff0c;发现市场还是挺火热的&#xff0c;虽说铜三铁四&#xff0c;但是软件测试岗位并没有削减多少&#xff0c;建议大家有空还是多关注和多投简历&#xff0c;不要闭门造车&#xff0c;错过好…

Cmake工具的简单使用

引言 本篇文章讲述如何简单的使用cmake工具构建一个项目&#xff0c;帮助入门的c新手学会如何使用cmake. 我们在Clion新创建一个项目时&#xff0c;会发现&#xff0c;除了main.cpp文件之外&#xff0c;还存在一个build-debug目录和一个CMakelists.txt文件&#xff0c;如图: …

一小时让你Get到面试套路:记一次Java初中级程序员面试流程梳理

视频教程传送门&#xff1a; 一小时让你Get到面试套路&#xff1a;记一次Java初中级程序员面试流程梳理_哔哩哔哩_bilibili听了N多个师兄师姐的面试录音&#xff0c;采访了N多个师兄时间的面试经历&#xff0c;才总结出来的java面试流程&#xff0c;非常适合正在准备面试的你。…

【7 微信小程序学习 - 小程序的系统API调用,网络请求封装】

1 网络请求 – 原生 请求数据,保存数据 1 原生请求 Page({data: {allCities: {},houselist: [],currentPage: 1},async onLoad() {// 1.网络请求基本使用wx.request({url: "http://codercba.com:1888/api/city/all",success: (res) > {//保存数据const data res…

【数据结构与算法】掌握顺序栈:从入门到实践

&#x1f331;博客主页&#xff1a;青竹雾色间. &#x1f331;系列专栏&#xff1a;数据结构与算法 &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞⭐收藏➕关注 目录 前言 顺序栈的实现 初始化栈 判断栈空 判断栈满 入&#xff08;进&#xff09;栈 出栈 获取栈…

P2[1-2]STM32简介(stm32简介+ARM介绍+片上外设+命名规则+系统结构+引脚定义+启动配置+最小系统电路+实物图介绍)

1.1 stm32简介 解释:ARM是核心部分,程序的内核,加减乘除的计算,都是在ARM内完成。 智能车方向:循迹小车,读取光电传感器或者摄像头数据,驱动电机前进和转弯。 无人机:用STM32读取陀螺仪加速度计的姿态数据,根据控制算法控制电机的速度,从而保证飞机稳定飞行。 机…

这 3个Python 函数你知道吗?

动动发财的小手&#xff0c;点个赞吧&#xff01; 作为21世纪最流行的语言之一&#xff0c;Python当然有很多有趣的功能值得深入探索和研究。今天将介绍其中的三个&#xff0c;每个都从理论上和通过实际示例进行介绍。 我想要介绍这些函数的主要原因是它们可以帮助您避免编写循…