Java多线程死锁避免方法

1、什么是死锁
当两个或两个以上的线程在执行过程当中,由于争夺资源而形成的一种相互等待的状态,因为存在一种环路的锁依赖关系而永远地等待下去,若是没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。
经典的 “哲学家进餐” 问题很好地描述了死锁情况:
5个哲学家去吃中餐,坐在一张圆桌旁,他们有5根筷子(而不是5双),而且每两我的中间放一根筷子,哲学家们要么在思考,要么
在进餐,每一个人都须要一双筷子才能吃到东西,并在吃完后将筷子放回原处继续思考,有些筷子管理算法 (1) 可以使每一个人都能相对及
时的吃到东西,但有些算法却可能致使一些或者全部哲学家都"饿死",后一种状况将产生死锁:每一个人都拥有其余人须要的资源,
同时有等待其余人已经拥有的资源,而且每一个人在获取全部须要的资源以前都不会放弃已经拥有的资源。
筷子管理算法(1):一个饥饿的科学家会尝试得到两根临近的筷子,但若是其中一根正在被另外一个科学家使用,那么他将放弃已经获得的
那根筷子,并在等待几分钟以后尝试
死锁:每一个人都当即抓住本身左边的筷子,而后等待本身右边的筷子空出来,但同时又不放下已经拿到的筷子,造成一种相互等待的状态。
饥饿:哲学家们都同时想吃饭,同时拿起左手边筷子,可是发现右边没有筷子,因而哲学家又同时放下左手边筷子,而后你们发现又有筷子了,又同时开始拿起左手边筷子,又同时放下,而后反复进行。
在线程A持有锁L并想得到锁M的同时,线程B持有锁M并尝试得到锁L,那么这两个线程将永远地等待下去,这种状况就是死锁形式(或者称为"抱死").
2、死锁的四个必要条件
互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。若是此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
请求和保持条件:指进程XM代理申请www.kaifx.cn/broker/xm.html已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对本身已得到的其它资源保持不放。
不剥夺条件:指进程已得到的资源,在未使用完以前,不能被剥夺,只能在使用完时由本身释放。
环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。
3、死锁实例
/**html

  • 死锁类示例
    */
    public class DeadLock implements Runnable {
    public int flag = 1;
    //静态对象是类的全部对象共享的
    private static Object o1 = new Object(), o2 = new Object();br/>@Overridepublic void run() {System.out.println("flag:{}"+flag);if (flag == 1) { //先锁o1,再对o2加锁,环路等待条件synchronized (o1) {try {Thread.sleep(500);} catch (Exception e) {e.printStackTrace();}synchronized (o2) {System.out.println("1");}}}if (flag == 0) {//先锁o2,在锁01synchronized (o2) {try {Thread.sleep(500);} catch (Exception e) {e.printStackTrace();}synchronized (o1) {System.out.println("0");}}}}public static void main(String[] args) {DeadLock td1 = new DeadLock();DeadLock td2 = new DeadLock();td1.flag = 1;td2.flag = 0;//td1,td2都处于可执行状态,但JVM线程调度先执行哪一个线程是不肯定的。//td2的run()可能在td1的run()以前运行new Thread(td1).start();new Thread(td2).start();}}一、当DeadLock 类的对象flag=1时(td1),先锁定o1,睡眠500毫秒二、而td1在睡眠的时候另外一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒三、td1睡眠结束后须要锁定o2才能继续执行,而此时o2已被td2锁定;四、td2睡眠结束后须要锁定o1才能继续执行,而此时o1已被td1锁定;五、td一、td2相互等待,都须要获得对方锁定的资源才能继续执行,从而死锁。动态锁顺序死锁:// 资金转帐到帐号public static void transferMoney(Account fromAccount,Account toAccount,DollarAmount amount)throws InsufficientFundsException {// 锁定汇款者的帐户synchronized (fromAccount) {// 锁定到帐者的帐户synchronized (toAccount) {// 判断帐户的余额不能为负数if (fromAccount.getBalance().compareTo(amount) < 0) {throw new InsufficientFundsException();} else {// 汇款者的帐户减钱fromAccount.debit(amount);// 到帐者的帐户增钱toAccount.credit(amount);}}}}上面的代码看起来都是按照相同的顺序来得到锁的,按道理来讲是没有问题,可是上述代码中上锁的顺序取决于传递给transferMoney()的参数顺序,而这些参数顺序又取决于外部的输入若是两个线程(A和B)同时调用transferMoney()其中一个线程(A),从X向Y转帐:transferMoney(myAccount,yourAccount,10);另外一个线程(B),从Y向X转帐 :transferMoney(yourAccount,myAccount,20);此时 A线程 可能得到 myAccount 的锁并等待 yourAccount的锁,然而 B线程 此时已经持有 yourAccount 的锁,而且正在等待 myAccount 的锁,这种状况下就会发生死锁。当一组java线程发生死锁的时候,那么这些线程永远不能再使用了,根据线程完成工做的不一样,可能会形成应用程序的彻底中止,或者某个特定的子系统不能再使用了,或者是性能下降,这个时候恢复应用程序的惟一方式就是停止并重启它,死锁形成的影响不多会当即显现出来,若是一个类发生死锁,并不意味着每次都会发生死锁,而只是表示有可能,当死锁出现的时候,每每是在最糟糕的时候——在高负载的状况下。4、死锁的避免与检测4.1 预防死锁破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行以前就申请须要的所有资源,且直至所要的资源所有获得知足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其余的资源。破坏不剥夺条件:即当某进程得到了部分资源,但得不到其它资源,则释放已占有的资源,可是只适用于内存和处理器资源。破坏循环等待条件:给系统的全部资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。4.2 设置加锁顺序若是两个线程(A和B),当A线程已经锁住了Z,而又去尝试锁住X,而X已经被线程B锁住,线程A和线程B分别持有对应的锁,而又去争夺其余一个锁(尝试锁住另外一个线程已经锁住的锁)的时候,就会发生死锁这样死锁就永远不会发生。 针对两个特定的锁,能够尝试按照锁对象的hashCode值大小的顺序,分别得到两个锁,这样锁老是会以特定的顺序得到锁,咱们经过设置锁的顺序,来防止死锁的发生,在这里咱们使用System.identityHashCode方法来定义锁的顺序,这个方法将返回由Obejct.hashCode 返回的值,这样就能够消除死锁发生的可能性。public class DeadLockExample3 {// 加时赛锁,在极少数状况下,若是两个hash值相等,使用这个锁进行加锁private static final Object tieLock = new Object();public void transferMoney(final Account fromAcct,final Account toAcct,final DollarAmount amount)throws InsufficientFundsException {class Helper {public void transfer() throws InsufficientFundsException {if (fromAcct.getBalance().compareTo(amount) < 0)throw new InsufficientFundsException();else {fromAcct.debit(amount);toAcct.credit(amount);}}}// 获得两个锁的hash值int fromHash = System.identityHashCode(fromAcct);int toHash = System.identityHashCode(toAcct);// 根据hash值判断锁顺序,决定锁的顺序if (fromHash < toHash) {synchronized (fromAcct) {synchronized (toAcct) {new Helper().transfer();}}} else if (fromHash > toHash) {synchronized (toAcct) {synchronized (fromAcct) {new Helper().transfer();}}} else {// 若是两个对象的hash相等,经过tieLock来决定加锁的顺序,不然又会从新引入死锁——加时赛锁synchronized (tieLock) {synchronized (fromAcct) {synchronized (toAcct) {new Helper().transfer();}}}}}}在极少数状况下,两个对象可能拥有两个相同的散列值,此时必须经过某种任意的方法来决定锁的顺序,不然可能又会从新引入死锁。为了不这种状况,可使用 “加时(Tie-Breaking))”锁,这得到这两个Account锁以前,从而消除了死锁发生的可能性4.3 支持定时的锁(超时放弃)有一项技术能够检测死锁和从死锁中恢复过来,就是使用Lock类中的定时public boolean tryLock(long time, TimeUnit unit) throws InterruptedException功能,来代替内置锁机制,当使用内置锁的时候,只要没有得到锁,就会永远等待下去,而tryLock能够指定一个超时时间(Timeout),在等待超过期间后tryLock会返回一个失败信息,若是超时时限比获取锁的时间要长不少,那么就能够在发生某个意外后从新得到控制权。以下图所示:4.4 死锁避免死锁防止方法可以防止发生死锁,但必然会下降系统并发性,致使低效的资源利用率,其中最具备表明性的避免死锁算法是银行家算法。一、多个资源的银行家算法检查一个状态是否安全的算法以下:查找右边的矩阵是否存在一行小于等于向量 A。若是不存在这样的行,那么系统将会发生死锁,状态是不安全的。倘若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。重复以上两步,直到全部进程都标记为终止,则状态时安全的。若是一个状态不是安全的,须要拒绝进入这个状态。4.5 死锁检测对资源的分配加以适当限制可防止或避免死锁发生,但不利于进程对系统资源的充分共享。为每一个进程和每一个资源指定一个惟一的号码Jstack命令jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的缘由,如线程间死锁、死循环、请求外部资源致使的长时间等待,线程出现停顿的时候经过jstack来查看各个线程的调用堆栈,就能够知道没有响应的线程到底在后台作什么事情,或者等待什么资源。JConsole工具Jconsole是JDK自带的监控工具,在JDK/bin目录下能够找到。它用于链接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。并且自己占用的服务器内存很小,甚至能够说几乎不消耗。 4.5 死锁恢复资源剥夺:剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除进程回退:根据系统保存的检查点让全部的进程回退,直到足以解除死锁,这种措施要求系统创建保存检查点、回退及重启机制进程撤销:一、撤销陷入死锁的全部进程,解除死锁,继续运行。二、逐个撤销陷入死锁的进程,回收其资源并从新分配,直至死锁解除。可选择符合下面条件之一的先撤销:1.CPU消耗时间最少者 2.产生的输出量最小者3.预计剩余执行时间最长者 4.分得的资源数量最少者后优先级最低者系统重启:结束全部进程的执行并从新启动操做系统。这种方法很简单,但先前的工做所有做废。
相关文章
相关标签/搜索