前言:
前面我们分析了为什么在多线程中会出现线程安全问题,并且也提出了一些解决的方法,多线程的编写里面中可以提高系统的利用率和处理能力,然而并发也带来了一系列严重的问题,其中之一就是死锁,下面我们来看看什么是死锁,什么情况下会产生死锁,以及死锁的一些避免方式。
一、什么是死锁:
在多线程中,死锁是指多个线程竞争抢占资源而导致一种僵直状态(也就是互相等待),若无外力干涉这些进程都无法向前推进:
二、产生死锁的条件:
在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(); } } }
测试代码:
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(); } }
打印结果:
线程Thread-1获取到了object2锁,方法Method2被调用 线程Thread-0获取到了object1锁,方法Method1被调用 线程Thread-1尝试虎丘object1锁,调用Method1方法…… 线程Thread-0尝试虎丘object2锁,调用Method2方法……
如上面代码所示,两个锁分别被两个线程所获取,但是又不释放,两个方法互相调用时,都会等待获取对方的锁,由此形成了死循环。
四、死锁的预防:
上面我们知道了产生死锁的条件,那么死锁预防的思路也就是在死锁放生之前破坏死锁发生的条件,四个条件中只要破坏一个就不会发生死锁了,那么来看一下我们应该如何预防死锁的出现:
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(); } } }
测试类:
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(); } }
输出结果:
线程Thread-0获取到了object1锁,开始调用Method3方法…… 线程Thread-0执行Method3方法完毕 线程Thread-0执行完毕 线程Thread-1获取到了object1锁,始调用Method3方法…… 线程Thread-1执行Method3方法完毕 线程Thread-1执行完毕 Process finished with exit code 0
当前成线程0调用Method1请求object1、object2锁时获取锁后,线程1如果这时调用Method2方法,由于线程1请求锁的顺序也是object1、object2,但此时锁却被线程0获取,所以进入 等待,只有线程0执行完毕后,线程1才会获取到锁,从而避免了死锁。
4、限时枷锁:
限时枷锁指的是在线程尝试获取锁的时候,我们加一个超时时间,若超过了这个时间则放弃对该锁的请求,并回退、释放所有已获得的锁,然后等待一段时间后在重试,
5、死锁的检测:
预防和避免死锁都有很大的开销,更好的方式使不采取任何措施,就是在我们多线程编写的时候不考虑死锁的方式,提供一种检测机制,能够检测到死锁的方法并采取适当的措施予以 清除
五、死锁的恢复:
既然线程会出现死锁的问题,那么必然有解决的方法,下面来看一下
1、利用抢占恢复:
临时将某个资源从他当前所属的进程中转移到另一个进程中,这一过程通常都需要人工干预,主要是否可行,取决与资源的特性。
2、利用回滚恢复:
周期性的将进程状态进行备份,当发现进程锁死后,根据备份将该进程复制到一个更早、还没有取得所需资源的状态,接着把这些资源分配给那些死锁的进程。
3、直接死进程恢复:
最简单直接的方法杀死一个或若干个进程,尽可能保证杀死的进程可以重头再来,而不受到影响。