死锁的产生和避免(一个和死锁相关的趣事)

死锁

死锁问题是在多进程或多线程环境中常见的一个复杂问题,它指的是两个或多个进程(线程)在执行过程中,因争夺资源而造成的一种僵局,若无外力作用,这些进程(线程)都将无法再向前推进。以下是对死锁问题的详细分析:

情景引入

  • 一天,一个饭桌前为了一群哲学家,每个哲学家之间放了一根筷子,桌子上放了一盘“意大利面”。
    在这里插入图片描述
  • 这些个哲学家只做两件事:思考人生或吃面条。思考人生的时候就就放下筷子,吃面条的时候就拿起左右两边的筷子(先拿右边后拿左边)。如果无法同时得到两根筷子,那就会阻塞等待。
    在这里插入图片描述
  • 突然间,这些哲学家都想要吃面,于是他们同时拿起右手的筷子,却发现左手的筷子已经没有了,所以他们都陷入了等待之中,可是却没有一个人能够吃到面·····
    在这里插入图片描述
    以上情景就是程序猿们津津乐道的“哲学家就餐问题”,它生动形象的展现了并发线程彼此相互等待对方所拥有的资源,从而导致各个线程不能继续进行的死锁问题

一、死锁的定义

死锁是指各并发进程(线程)彼此互相等待对方所拥有的资源,且这些并发进程(线程)在得到对方的资源之前不会释放自己所拥有的资源,从而造成大家都想得到资源而又都得不到资源,各并发进程(线程)不能继续向前推进的状态。

二、产生死锁的原因(面试考点!)

产生死锁的原因可以归结为多个进程(线程)在竞争资源时,由于竞争条件和不恰当的顺序,导致它们相互等待对方释放资源,从而形成一个循环等待的状态。具体来说,产生死锁的四个必要条件是:

  1. 互斥条件

    进程对所分配到的资源进行排他性使用,即在一段时间内,某资源只能被一个进程占用(同一时间一根筷子只能被一个哲学家使用)。如果此时还有其他进程请求该资源,则请求进程只能等待,直至占有该资源的进程用毕释放(等筷子被用完才能使用)。

  2. 请求与保持条件

    进程已经保持至少一个资源,又请求新的资源而失败,此时请求进程被阻塞,但对自己已获得的资源保持不放。(只拿到一根筷子的哲学家想要得到另一根筷子就要等待旁边的哲学家用完筷子

  3. 不可抢占条件

    进程已获得的资源在未使用完之前不能被抢占,只能在进程使用完时自己释放。(哲学家是文明人,就算拿不到两根筷子也不会去抢别人的

  4. 循环等待条件

    若干进程之间形成一种头尾相接的循环等待资源关系。(饭桌上的所有哲学家同时想吃面,并且每个哲学家都拿到了一根筷子

三、实例说明

public class Demo12 {
    private static int count = 0;
    public static void main(String[] args) {
        Object lockA = new Object();
        Object lockB = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                synchronized (lockB){
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                synchronized (lockA){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
    }
}

以两个线程为例,线程1持有锁A并请求锁B,而线程2持有锁B并请求锁A。此时,两个线程都无法继续执行,因为它们都在等待对方释放资源,从而形成了死锁。

四、死锁的预防

预防死锁是一种较简单和直观的事先预防的方法,通过破坏产生死锁的四个必要条件中的一个或多个来预防死锁的发生。例如:

  • 确保资源总是按照相同的顺序请求。(解决死锁的常见方式
  • 引入超时机制来中断等待。(等了一会如果还没有筷子空闲就放下筷子继续思考人生
  • 一次性分配所有需要的资源。(给每个哲学家都分配一双筷子

但需要注意的是,预防死锁的方法可能会降低系统的资源利用率和吞吐量

具体实现(以第一种方式为例):
最常⽤的⼀种死锁阻⽌技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号 (1, 2, 3…M).
N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.
在这里插入图片描述
哲学家两两为一组,共同使用一双筷子,规定必须先取得编号为1的筷子,才能拿编号为2的筷子,这样就保证了就餐时一定有一个人取得一双筷子。

代码

   Object lock1 = new Object();
   Object lock2 = new Object();
   Thread t1 = new Thread() {
       @Override
       public void run() {synchronized (lock1) {
           synchronized (lock2) {
               // do something...
           }
       }
       }
   };
   t1.start();
   Thread t2 = new Thread() {
       @Override
       public void run() {
           synchronized (lock1) {
               synchronized (lock2) {
                   // do something...
               }
           }
       }
   };
   t2.start();

五、死锁的避免

死锁避免是一种动态策略,系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源。如果分配后系统可能发生死锁,则不予分配,否则予以分配。这种方法可以在一定程度上提高系统的资源利用率,但实现起来较为复杂。

还有就是引入可重入锁(Reentrant Lock),也被称为递归锁,是一种允许同一个线程多次获得同一把锁的锁机制。这种锁机制在Java中非常重要,特别是在处理多线程同步问题时。以下是对可重入锁的详细解释:

  • 定义与特性

    • 定义:可重入锁指的是当一个线程已经持有某个对象锁时,该线程再次请求获取该对象锁时,该线程可以成功获取到锁,而不会发生死锁。
      特性:
    • 可重入性同一个线程可以多次获得同一把锁,而不会发生死锁
    • 递归调用在外层方法获取锁之后,内层递归方法仍然能获取该锁
    • 计数器内部通常使用计数器来记录锁被获取的次数,每次成功获取锁,计数器加1;每次释放锁,计数器减1。当计数器为0时,表示锁可用
  • synchronized关键字:Java内置的synchronized关键字就是一种隐式的可重入锁。当一个线程进入一个对象的synchronized方法时,
    它会自动获得该对象的锁,并且在方法执行期间,其他线程无法进入该对象的任何synchronized方法或代码块,除非该线程已经释放了锁。
    如果一个线程已经持有了某个对象的锁,并且它再次尝试获取该对象的锁,那么它可以成功获取,因为synchronized是可重入的。

  • 工作原理
    可重入锁的工作原理通常基于内部的一个计数器(或称为状态变量)。当线程首次成功获取锁时,计数器被设置为1。如果同一个线程再次请求获取锁,
    计数器会递增,表示该线程已经多次获得了锁。当线程释放锁时,计数器会递减。只有当计数器减至0时,锁才会被完全释放,此时其他线程才有机会获取该锁。

    示例

    public class Demo12 {
        private static int count = 0;
        public static void main(String[] args) {
            Object lock = new Object();
            Thread t = new Thread(() -> {
                // 获取同一个锁
                synchronized (lock) {
                    synchronized (lock){
                        count++;
                    }
                }
            });
            t.start();
        }
    }
    
  • 注意事项
    使用可重入锁时,必须确保在finally块中释放锁,以避免死锁的发生。
    可重入锁虽然可以避免死锁,但如果不当使用(如忘记释放锁),仍然可能导致程序出现严重问题。
    综上所述,可重入锁是Java中处理多线程同步问题的一种重要机制,它通过允许同一个线程多次获取同一把锁来避免死锁的发生,并提供了比synchronized关键字更多的灵活性和功能。

六、死锁的检测与解除

如果系统中既没有采取死锁预防措施,也未配有死锁避免算法,那么系统很可能会发生死锁。在这种情况下,系统应当提供死锁检测和解除算法:

  • 死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。检测方法包括定时检测、效率低时检测、进程等待时检测等。
  • 死锁解除算法:当认定系统中已发生了死锁,利用该算法可将系统从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。

七、总结

死锁问题是多进程(线程)并发执行时的一个难点,需要通过合理的资源分配策略、死锁预防和避免算法以及死锁检测和解除机制来有效解决。在实际应用中,需要根据具体场景和需求选择合适的方法来处理死锁问题。

  • 26
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值