当前位置: 首页 > 科技观察

多线程死锁详解

时间:2023-03-16 15:55:37 科技观察

1.什么是死锁?当两个或多个线程在执行时,由于争夺资源,它们处于相互等待的状态。循环的锁依赖永远等待。如果没有外部干预,他们将永远等待。此时的这种状态称为死锁。经典的“哲学家就餐”问题很好地描述了僵局的情况:5位哲学家去吃中餐,坐在一张圆桌旁,他们有5根筷子(而不是5双),并放在每两个人之间。一根筷子,哲学家要么在思考,要么在吃饭,每个人都需要一双筷子吃饭,吃完后把筷子放回原处继续思考,一些筷子管理算法(1)可以让每个人都可以相对的吃饭及时,但有些算法可能会导致部分或全部哲学家“饿死”。人们已经拥有的资源,每个人都不会放弃自己拥有的资源,直到他获得了他所需要的所有资源。筷子管理算法(一):一个饥饿的科学家会尝试去拿两根相邻的筷子,但是如果其中一根正被另一位科学家使用,那么他就会放弃自己已经拿到的筷子,等待几分钟再尝试死锁:大家立即拿起左手筷子,等待右手筷子腾出,但同时已经拿到的筷子也不放下,形成相互等待的状态。饥饿:哲学家们同时都想吃饭,同时拿起左手的筷子,却发现右边没有筷子,于是哲学家们同时放下了左手的筷子次,然后大家又发现有筷子,开始同时拿起左手的筷子,同时又放下,然后重复。当线程A持有锁L想要获取锁M,而线程B持有锁M想要获取锁L,那么这两个线程就会一直等待下去。这种情况是死锁(或“死锁”)的一种形式2.死锁的四个必要条件互斥条件:指一个进程独占使用所分配的资源,即某一资源在其内只被一个进程占用一段时间。如果此时有其他进程在请求资源,请求者只能等到占用资源的进程用完释放。请求和持有条件:表示一个进程至少保留了一个资源,但是又提出了新的资源请求,并且该资源已经被其他进程占用。此时,请求进程被阻塞,但它仍然持有它已经获得的其他资源。非剥夺条件:指进程已经获取的资源,在用完之前不能剥夺,用完只能自己释放。循环等待条件:发生死锁时,必然有一个进程——资源循环链,即进程集{A,B,C,...,Z}中的A正在等待B占用的资源;B在等待一个被C占用的资源,...,Z在等待一个已经被A占用的资源。3.死锁实例/***死锁类示例*/publicclassDeadLockimplementsRunnable{publicintflag=1;//静态对象是privatestaticObjecto1=newObject(),o2=newObject();@Overridepublicvoidrun(){System.out.println("flag:{}"+flag);if(flag==1){//先锁o1,再锁o2,循环等待条件synchronized(o1){try{Thread.sleep(500);}catch(Exceptione){e.printStackTrace();}synchronized(o2){System.out.println("1");}}}if(flag==0){//先锁o2,锁01synchronized(o2){try{Thread.sleep(500);}catch(Exceptione){e.printStackTrace();}synchronized(o1){System.out.println("0");}}}}publicstaticvoidmain(String[]args){DeadLocktd1=newDeadLock();DeadLocktd2=newDeadLock();td1.flag=1;td2.flag=0;//td1,td2都处于可执行状态,但JVM线程调度不确定哪个线程先执行。//td2的run()可能先于td1的run()newThread(td1).start();newThread(td2).start();}}1.当DeadLock类(td1)的对象flag=1时,首先locko1,sleep500毫秒2,td1在休眠的时候,另外一个flag==0的对象(td2)线程启动,先locko2,sleep500毫秒3,td1休眠后,需要locko2才能继续执行,此时o2已经被td2锁定;4、td2休眠后,o1需要加锁才能继续执行,此时o1已经被td1加锁;5、td1和td2相互等待,都需要获取对方锁定的资源才能继续执行。从而陷入僵局。动态锁序列死锁://资金转入账户publicstaticvoidtransferMoney(AccountfromAccount,AccounttoAccount,DollarAmountamount)throwsInsufficientFundsException{//锁定汇款人账户synchronized(fromAccount){//锁定收款人账户synchronized(toAccount){//判断余额theaccountcannotbenegativeif(fromAccount.getBalance().compareTo(amount)<0){thrownewInsufficientFundsException();}else{//汇款人的账户减钱fromAccount.debit(amount);//收款人的AddmoneytoAccount.credit(amount);}}}}上面的代码好像是按照相同的顺序获取锁,这是合理的,但是上面代码中加锁的顺序取决于传递给transferMoney()的参数的顺序,其中turndependsonexternalinputs如果两个线程(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防止死锁破坏互斥条件:使资源同时访问而不是互斥,这样就没有进程阻塞在资源上,所以不会发生死锁。销毁请求和保留条件:静态分配静态分配的方式是指进程在执行前必须申请所有需要的资源,直到所有需要的资源都满足后才会开始执行。只要一个资源不能分配,其他资源就不会分配给这个进程。H.销毁非剥夺条件:即当一个进程获得了一些资源但不能获得其他资源时,释放占用的资源,但只适用于内存和处理器资源。破坏循环等待条件:给系统所有的资源编号,规定进程请求所需资源的顺序必须按照资源的编号顺序。4.2设置锁顺序如果有两个线程(A和B),当A线程已经锁定了Z,然后尝试锁定X,而X已经被线程B锁定,线程A和线程B分别持有对应的A就会发生死锁当竞争另一个锁时(试图加锁一个已经被另一个线程加锁的锁),如下图所示:两个线程以不同的顺序尝试获取同一个锁,如果Request以相同的顺序加锁,那么就会出现没有循环锁依赖,所以不会出现死锁,每个需要锁Z和锁X的线程都会按照相同的顺序获取Z和X,那么就不会出现死锁,如下图所示:,死锁永远不会发生。对于特定的两把锁,可以尝试按照锁对象的hashCode值的顺序分别获取两把锁,这样锁总会按照特定的顺序获取。我们可以通过设置锁的顺序来防止死锁,这里我们使用System.identityHashCode方法来定义锁的顺序,该方法会返回Object.hashCode返回的值,这样就可以消除死锁的可能性。publicclassDeadLockExample3{//超时锁,在极少数情况下,如果两个哈希值相等,则使用此锁加锁privatestaticfinalObjecttieLock=newObject();publicvoidtransferMoney(finalAccountfromAcct,finalAccounttoAcct,finalDollarAmountamount)throwsInsufficientFundsException{classHelper{publicvoidthrowstransfers(){if(fromAcct.getBalance().compareTo(金额)<0)thrownewInsufficientFundsException();else{fromAcct.debit(金额);toAcct.credit(金额);}}}//得到两把锁的hash值intfromHash=System.identityHashCode(fromAcct);inttoHash=System.identityHashCode(toAcct);//根据hash值判断锁序,如果是则判断锁序(fromHashtoHash){synchronized(toAcct){synchronized(fromAcct){newHelper().transfer();}}}else{//如果两个对象的hash值相等,tieLock决定加锁的顺序,否则会重新引入死锁——超时加锁synchronized(tieLock){synchronized(fromAcct){synchronized(toAcct){newHelper().transfer();}}}}}}在极少数情况下,两个对象可能有两个相同的哈希值。这时,必须使用一些任意的方法来确定锁的顺序,否则可能为了避免这种情况会重新引入死锁,可以使用“超时(Tie-Breaking))”锁,这样就消除了在两个Account锁都未获得之前出现死锁的可能4.3支持定时锁(Timeoutabandonment)有一种检测死锁并从死锁中恢复的技术,即使用Lock类中常规的publicbooleantryLock(longtime,TimeUnitunit)throwsInterruptedException函数来代替内置的锁机制。使用内置锁时,只要没有获得锁,就会一直等待,而tryLock可以指定超时时间(Timeout),等待超时后,tryLock会返回失败信息,如果超时时间为比获取锁的时间长得多,那么你可以在发生意外后重新获得控制权。如下图所示:4.4死锁避免死锁预防方法可以防止死锁,但不可避免地会降低系统并发度,导致资源利用效率低下。最具代表性的死锁避免算法是银行家算法。1.多资源银行家算法上图中有5个进程,4个资源。左边的图代表已经分配的资源,右边的图代表还需要分配的资源。最右边的E、P、A分别代表:总资源、分配资源、可用资源。请注意,这三个是向量,而不是具体值。例如A=(1020),表示剩余4个资源为1/0/2/0。检查一个状态是否安全的算法如下:查找右边矩阵中是否有小于等于向量A的行,如果不存在则系统死锁,状态不安全.如果找到这样的一行,则将该进程标记为终止,并将其分配的资源添加到A。重复以上两步,直到所有进程都标记为终止,则状态是安全的。如果一个状态不安全,你需要拒绝进入这个状态。4.5死锁检测适当限制资源的分配可以防止或避免死锁,但不利于进程充分共享系统资源。为每个进程和每个资源指定一个唯一的编号jstack命令jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机中各个线程正在执行的方法栈的集合。生成线程快照的主要目的是定位导致线程长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的时间过长等。等待时间,当线程暂停时,通过jstack查看各个线程的调用栈,可以知道没有响应的线程在后台干什么,或者等待什么资源JConsole工具Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。用于连接正在运行的本地或远程JVM,监控运行Java应用程序的资源消耗和性能,绘制大量图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。4.5死锁恢复资源剥夺:剥夺陷入死锁的进程占用的资源,但在死锁解除之前不撤销进程。进程回滚:根据系统保存的检查点,回滚所有进程,直到足以解除死锁。锁,这个措施需要系统建立保存检查点、回滚和重启机制。进程取消:1.取消所有陷入死锁的进程,解除死锁,继续运行。2.将死锁的进程一一取消,回收它们的资源并重新分配,直到死锁解除。您可以选择满足以下条件之一的先取消:消耗CPU时间最少的产生最小的输出,估计剩余执行时间最长的获得最少的资源,并且系统重启后优先级最低:结束所有进程的执行并重启操作系统。这个方法很简单,但是之前的所有工作都作废了。