Java并发编程之死锁相关知识整理
一、什么是死锁
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进
二、死锁产生的条件
以下将介绍死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁
互斥条件
进程要求对所分配的资源(如打印机〉进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待
不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)
请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放
循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求s即存在一个处于等待状态的进程集合{PI, P2,…,, pn}
其中Pi等待的资源被P(i+1)占有( i=0,1,… , n-1),n等待的资源被Po占有
但也有可能Pi等待的资源被P(i+1)占有( i=0,1,… , n-1),但可以通过圈外也获取资源(不死锁),如图所示
三、死锁产生的演示
接下来我们创建示例类,通过不同线程来获取不同的锁看看
public class Deadlock implements Runnable { private int flag;//用于区分走向 //对象锁 static 使不同线程引用的都是同一地址 private static Object obj1 =new Object(); //对象锁 static 使不同线程引用的都是同一地址 private static Object obj2 =new Object(); public Deadlock(int flag) { this.flag = flag; } public void run(){ if(flag == 1){ synchronized (obj1){ System.out.println(Thread.currentThread().getName () + "获取Obj1,需要请求Obj2"); try{ Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj2){ System.out.println(Thread.currentThread().getName () + "已获取Obj1、获取Obj2"); } } }else{ synchronized (obj2){ System.out.println(Thread.currentThread().getName () + "获取Obj2,需要请求Obj1"); try{ Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj1){ System.out.println(Thread.currentThread().getName () + "已获取Obj2、获取Obj1"); } } } } }
这时我们创建两个线程, 执行这两个obj的锁,看看是否会产生死锁
class DeadlockTest { public static void main(String[] args) { Thread thread1 = new Thread(new Deadlock(1),"线程1"); Thread thread2 = new Thread(new Deadlock(2),"线程2"); thread1.start(); thread2.start(); } } //运行结果如下: 线程1获取Obj1,需要请求Obj2 线程2获取Obj2,需要请求Obj1
我们发现并没有已获取obj1、obj2或者以获取obj2、获取obj1 的输出,因为他们满足了死锁产生的条件
四、死锁的预防
预防死锁是设法至少破坏产生死锁的四个必要条件之一严格的防止死锁的出现
破坏互斥条件
“互斥”条件是无法破坏的。在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件
破坏“占有并等待”条件
破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源
即要想出一个办法,阻止进程在持有资源的同时申请其他资源,有以下思路可提供:
- 方法一:
即创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它
- 方法二:
要求每个进程提出新的资源申请前,释放它所占有的资源
这样一个进程在需要资源A时,须先把它先前占有的资源R释放掉,然后才能提出对A的申请,即使它可能很快又要用到资源R
破坏“不可抢占”条件
破坏“不可抢占”条件就是允许对资源实行抢夺
如果占有某些资源的一个进程进行下一步资源请求被拒绝,则该进程必须释放它最初占有的资源
,如果有必要,可再次请求这些资源和另外的资源
如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁
破坏“循环等待”条件
破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
五、死锁的避免
死锁的语法是是严格限制产生死锁的条件,避免死锁的方式不严格限制,因为即使死锁的必要条件存在,也不一定发生死锁。而是让程序通过算法再满足条件后避免死锁
避免方法:有序资源分配算法
该算法实现步骤如下:
- 必须为所有资源统一编号,例如打印机为1、传真机为2、磁盘为3等
- 同类资源必须一次申请完,例如打印机和传真机一般为同一个机器必须同时申请
- 不同类资源必须按顺序申请
举例:有两个进程P1和P2,有两个资源R1和R2,P1与P2线程、分别请求资源:R1、R2
P1先获取R1、R2,而P2就请求等待P1释放,这样就破坏了环路条件,避免了死锁的发生
避免方法:银行家算法
银行家算法(Banker's A1gorithm)是一个避免死锁(Dead1ock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.HE系统设计的一种避免死锁产生的算法
它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。流程图如下:
避免方法:顺序加锁
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生
我们上面的示例代码就是这样的情况,线程1请求Obj1、Obj2,线程2请求Obj2、Obj1
而我们如果能够保证所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生
列如我们线程1请求Obj1、Obj2,线程2请求Obj1、Obj2
按照顺序加锁是一种有效的死锁预防机制。但是这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的,所以该种方式只适合特定场景
避免方法:限时加锁
限时加锁是线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,并回退并释放所有已经获得的锁
,然后等待一段随机的时间再重试
以下展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回很并重试
的场景:
//线程 1 锁定A Thread 1 locks A //线程 2 锁定B Thread 2 locks B //线程 1 尝试去锁定B,但已被锁定 Thread 1 attempts to lock 8 but is blocked //线程 2 尝试去锁定A,但已被锁定 Thread 2 attempts to lock A but is blocked //线程 1 等待锁定B的时间超时了 Thread 1' s lock attempt on B times out //线程 1 进行回退并释放锁定A的资源 Thread 1 backs up and releases A as well //线程 1 等待一段时间再重试获取 Thread 1 waits randomly (e.g. 257 millis) before retrying Thread 2's lock attempt on A times out Thread 2 backs up and releases B as well Thread 2 waits randomly (e.g.43 millis) before retrying
在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁
,这时线程1尝试获取锁A并且处于等待状态,当线程2结束时,线程1也可以顺利的获得这两个锁
这种方式有两个缺点:
- 当线程数量少时,该种方式可避免死锁,但
当线程数量过多,这些线程的加锁时限相同的概率就高很多,可能会导致超时后重试的死循环
- Java中
不能对synchronized同步块设置超时时间
,你需要创建自定义锁或使用Java5中 java .util.concurrent包下的工具
到此这篇关于Java并发编程之死锁相关知识整理的文章就介绍到这了,更多相关Java死锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!