• 聊聊“死锁“


    “死锁”或者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线程死锁的例子,涉及两个线程和两个共享资源:

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在这个例子中,线程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()方法,可以在获取锁失败时立即释放已经获取的锁,避免发生死锁。

    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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

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

    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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
  • 相关阅读:
    linux 压缩命令之tar工具的基本使用
    荧光染料CY3/CY5/CY5.5聚已内酯PLA载药纳米粒CY3-PLA|CY5-SS-PEG-PLA|CY5.5-PLA(定制供应)
    单链表基本操作-查找
    SpringBoot+MySQL+Vue前后端分离的宠物领养救助管理系统(附论文)
    uniapp公共新闻模块components案例
    CesiumJS 2022^ 源码解读[7] - 3DTiles 的请求、加载处理流程解析
    前端菜鸡学习日记 -- computed和watch的用法
    oracle数据库常见巡检脚本-系列一
    kafka---springboot
    Spring Boot中JUnit 4与JUnit 5的如何共存
  • 原文地址:https://blog.csdn.net/sinat_34206747/article/details/131135408