六、线程死锁

前言:

  前面我们分析了为什么在多线程中会出现线程安全问题,并且也提出了一些解决的方法,多线程的编写里面中可以提高系统的利用率和处理能力,然而并发也带来了一系列严重的问题,其中之一就是死锁,下面我们来看看什么是死锁,什么情况下会产生死锁,以及死锁的一些避免方式。

一、什么是死锁:

  在多线程中,死锁是指多个线程竞争抢占资源而导致一种僵直状态(也就是互相等待),若无外力干涉这些进程都无法向前推进:

二、产生死锁的条件:

  在java中死锁的出现也不是凭空实现的,死锁的出现必然会满足一下这些条件,我们来看一下这些条件都有哪些:

    1、互斥条件:

      进程要求对所分配的资源(如打印机)进行排他控制,即在一段时间内某资源仅为一个进程锁占有,此时若有其他进程请求该资源,则进行等待。

    2、不可剥夺条件:

      进程在获取到资源,在没有使用完毕前,不能被其他进程强行剥夺,只能有使用该资源的进程主动释放。

    3、请求与保持条件:

      进程已经保持了至少一个资源,但又提出的新的请求,而该资源已被其他进程占用,此时请求会被阻塞,但对自己获得的资源又保持不放。

    4、循环等待条件:

      存在一种进程资源的循环等待环,环中的每一个进程都获得资源的时候被环中下一个进程锁请求,即存在一个处于等待的状态的进行集合(p0、p1……pn),p0等待p1,p1等待p2,一直到pn,但是pn等待的资源却又被p0占用,第二幅图自是一种不蛮子死循环的循环环,因为在pn的时候如果从p0拿不到资源,它还可以自pk获取资源。

       

三、死锁代码的演示:

  这里简单模拟一下死锁的环境,来看一下代码中出现死锁的情况是怎样的,就拿两个线程拥有各自的锁,却互相调用来举例,代码如下:

public class DeadlockDemo {


    private static Object object1 = new Object();
    private static Object object2 = new Object();

    static class Deadlock1{

        public void Method1(){
            synchronized (object1){
                try {
                    System.out.println("线程" + Thread.currentThread().getName()+
                            "获取到了object1锁,方法Method1被调用");
                    Thread.sleep(1000);
                    System.out.println("线程" + Thread.currentThread().getName()+
                            "尝试虎丘object2锁,调用Method2方法……");
                    Method2();
                    System.out.println("线程" + Thread.currentThread().getName()+
                            "执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }

        public void Method2(){
            synchronized (object2){
                try {
                    System.out.println("线程" + Thread.currentThread().getName()+
                            "获取到了object2锁,方法Method2被调用");
                    Thread.sleep(1000);
                    System.out.println("线程" + Thread.currentThread().getName()+
                            "尝试虎丘object1锁,调用Method1方法……");
                    Method1();
                    System.out.println("线程" + Thread.currentThread().getName()+
                            "执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Deadlock2 implements Runnable{
        private Deadlock1 deadlock;
        public Deadlock2(Deadlock1 deadlock){
            this.deadlock = deadlock;
        }
        @Override
        public void run() {
            deadlock.Method1();

        }
    }

    static class Deadlock3 implements Runnable{
        private Deadlock1 deadlock;
        public Deadlock3(Deadlock1 deadlock){
            this.deadlock = deadlock;
        }

        @Override
        public void run() {
            deadlock.Method2();
        }
    }
}
View Code

  测试代码:

public class ThreadDemo {

    public static void main(String[] args) {
        DeadlockDemo.Deadlock1 deadlock1 = new DeadlockDemo.Deadlock1();
        DeadlockDemo.Deadlock2 deadlock2 = new DeadlockDemo.Deadlock2(deadlock1);
        DeadlockDemo.Deadlock3 deadlock3 = new DeadlockDemo.Deadlock3(deadlock1);

        Thread thread1 = new Thread(deadlock2);
        Thread thread2 = new Thread(deadlock3);
        thread1.start();
        thread2.start();
    }
}
View Code

  打印结果:

线程Thread-1获取到了object2锁,方法Method2被调用
线程Thread-0获取到了object1锁,方法Method1被调用
线程Thread-1尝试虎丘object1锁,调用Method1方法……
线程Thread-0尝试虎丘object2锁,调用Method2方法……
View Code

  如上面代码所示,两个锁分别被两个线程所获取,但是又不释放,两个方法互相调用时,都会等待获取对方的锁,由此形成了死循环。

四、死锁的预防:

  上面我们知道了产生死锁的条件,那么死锁预防的思路也就是在死锁放生之前破坏死锁发生的条件,四个条件中只要破坏一个就不会发生死锁了,那么来看一下我们应该如何预防死锁的出现:

  1、破坏“互斥”条件:

    互斥条件是无法被破坏的,因此预防死锁都是破坏其他三种条件,比如说打印进一个人使用其他人就不能使用了,是硬件上的就是如此设计的,是不能人为破坏的

  2、破坏“占有并等待”条件:

    破坏占有并等待条件,是指一个进程在占用了一个资源后,一般情况下不允许他在申请其他资源,破坏方式一般有一下两种方式:

    (1):一次性分配资源,即创建进程时,他需要多少资源都分配给他,以满足要求,后续不会再变动。

    (2):要求每个进程在提出新的要求时,要释放自己所获取的资源,这样一个进程拥有A资源时,申请B资源就要释放A,哪怕他很快又要使用A资源。

  3、破坏“不可抢占”条件:

    破坏不可抢占资源,指一个进程获取了此资源,那么其它进程要想使用此资源就必须进行等待,破坏方式有如下两种方式:

    (1):占有某个资源的进程如果申请其他资源,那么这个进程必须释放他锁占有的资源,如果有必要它可以再次请求这个资源和其他资源。

    (2):如果进程A请求的资源正好被B进程所占有,则操作系统可以抢占其他进程,要求B释放资源,只有在任意两个进程的优先级都不相同时时候,才可以预防死锁

  4、破坏“循环等待”条件:

    破坏循环等待条件是指,将系统中的资源进行编号,进程可以在任意时刻进行资源申请,但所申请必须按照资源编号来,这样就可以便面死锁,例如我们最开始的例子,如果把锁object1和object2编号为1、2,Method1和Method2方法请求锁顺序相同,就不会出现死锁,下面死锁的避免中有具体代码演示。

五、死锁的避免:

  了解了死锁的产生和预防,来看一下死锁的避免有哪些方式,这其中有些就是上面死锁的预防的一些条件

  1、有序的资源分配法:

    必须为所有的资源统一编号,例如打印机为1,传真机为2,磁盘为3等:

    同类资源必须一次性请求完成

    不同的资源必须按照顺序请求:

  2、银行家算法:

    银行家算法是一个避免死锁的著名算法,在这里只是做一下了解:

  3、顺序枷锁:

    当多个线程需要相同的锁的时候,如果按照不同的顺序枷锁,死锁就很容易发生死锁,就如我们上面的例子,线程0请求object1、object2锁,线程1请求object2、object1锁,就出现了  死锁的情况。如果按照线程0请求object1、object2锁时,线程2则是等待object1、object2锁则就不会发生死锁的情况了 ,看一下代码:

public class DeadlockDemo {


    private static Object object1 = new Object();
    private static Object object2 = new Object();

    static class Deadlock1{

        public void Method1(){
            synchronized (object1){
                System.out.println("线程" + Thread.currentThread().getName()+
                        "获取到了object1锁,开始调用Method3方法……");
                Method3();
                System.out.println("线程" + Thread.currentThread().getName()+
                        "执行完毕");
            }
        }

        public void Method2(){
            synchronized (object1){
                System.out.println("线程" + Thread.currentThread().getName()+
                        "获取到了object1锁,始调用Method3方法……");
                Method3();
                System.out.println("线程" + Thread.currentThread().getName()+
                        "执行完毕");
            }
        }

        public void Method3(){
            synchronized (object2){
                System.out.println("线程" + Thread.currentThread().getName()+
                        "执行Method3方法完毕");
            }
        }
    }

    static class Deadlock2 implements Runnable{
        private Deadlock1 deadlock;
        public Deadlock2(Deadlock1 deadlock){
            this.deadlock = deadlock;
        }
        @Override
        public void run() {
            deadlock.Method1();

        }
    }

    static class Deadlock3 implements Runnable{
        private Deadlock1 deadlock;
        public Deadlock3(Deadlock1 deadlock){
            this.deadlock = deadlock;
        }

        @Override
        public void run() {
            deadlock.Method2();
        }
    }
}
View Code

  测试类:

public class ThreadDemo {

    public static void main(String[] args) {
        DeadlockDemo.Deadlock1 deadlock1 = new DeadlockDemo.Deadlock1();
        DeadlockDemo.Deadlock2 deadlock2 = new DeadlockDemo.Deadlock2(deadlock1);
        DeadlockDemo.Deadlock3 deadlock3 = new DeadlockDemo.Deadlock3(deadlock1);

        Thread thread1 = new Thread(deadlock2);
        Thread thread2 = new Thread(deadlock3);
        thread1.start();
        thread2.start();
    }
}
View Code

  输出结果:

线程Thread-0获取到了object1锁,开始调用Method3方法……
线程Thread-0执行Method3方法完毕
线程Thread-0执行完毕
线程Thread-1获取到了object1锁,始调用Method3方法……
线程Thread-1执行Method3方法完毕
线程Thread-1执行完毕

Process finished with exit code 0
View Code

    当前成线程0调用Method1请求object1、object2锁时获取锁后,线程1如果这时调用Method2方法,由于线程1请求锁的顺序也是object1、object2,但此时锁却被线程0获取,所以进入  等待,只有线程0执行完毕后,线程1才会获取到锁,从而避免了死锁。

  4、限时枷锁:

    限时枷锁指的是在线程尝试获取锁的时候,我们加一个超时时间,若超过了这个时间则放弃对该锁的请求,并回退、释放所有已获得的锁,然后等待一段时间后在重试,

  5、死锁的检测:

    预防和避免死锁都有很大的开销,更好的方式使不采取任何措施,就是在我们多线程编写的时候不考虑死锁的方式,提供一种检测机制,能够检测到死锁的方法并采取适当的措施予以  清除

五、死锁的恢复:

  既然线程会出现死锁的问题,那么必然有解决的方法,下面来看一下

  1、利用抢占恢复:

    临时将某个资源从他当前所属的进程中转移到另一个进程中,这一过程通常都需要人工干预,主要是否可行,取决与资源的特性。

  2、利用回滚恢复:

    周期性的将进程状态进行备份,当发现进程锁死后,根据备份将该进程复制到一个更早、还没有取得所需资源的状态,接着把这些资源分配给那些死锁的进程。

  3、直接死进程恢复:

    最简单直接的方法杀死一个或若干个进程,尽可能保证杀死的进程可以重头再来,而不受到影响。

 

转载于:https://www.cnblogs.com/zouxiangzhongyan/p/11486582.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值