Java语言通过synchronized关键字来保证原子性,这是因为每一个Object都有一个隐含的锁,这个也称作监视器对象。在进入synchronized之前自动获取此内部锁,而一旦离开此方式,无论是完成或者中断都会自动释放锁。显然这是一个独占锁,每个锁请求之间是互斥的。相对于众多高级锁(Lock/ReadWriteLock等),synchronized的代价都比后者要高。但是synchronzied的语法比较简单,而且也比较容易使用和理解。Lock一旦调用了lock()方法获取到锁而未正确释放的话很有可能造成死锁,所以Lock的释放操作总是跟在finally代码块里面,这在代码结构上也是一次调整和冗余。Lock的实现已经将硬件资源用到了极致,所以未来可优化的空间不大,除非硬件有了更高的性能,但是synchronized只是规范的一种实现,这在不同的平台不同的硬件还有很高的提升空间,未来Java锁上的优化也会主要在这上面。既然synchronzied都不可能避免死锁产生,那么死锁情况会是经常容易出现的错误,下面具体描述死锁发生的原因及解决方法。
死锁描述死锁是操作系统层面的一个错误,是进程死锁的简称,最早在年由Dijkstra在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。
事实上,计算机世界有很多事情需要多线程方式去解决,因为这样才能最大程度上利用资源,才能体现出计算的高效。但是,实际上来说,计算机系统中有很多一次只能由一个进程使用的资源的情况,例如打印机,同时只能有一个进程控制它。在多通道程序设计环境中,若干进程往往要共享这类资源,而且一个进程所需要的资源还很有可能不止一个。因此,就会出现若干进程竞争有限资源,又推进顺序不当,从而构成无限期循环等待的局面。我们称这种状态为死锁。简单一点描述,死锁是指多个进程循环等待它方占有的资源而无限期地僵持下去的局面。很显然,如果没有外力的作用,那么死锁涉及到的各个进程都将永远处于封锁状态。
系统发生死锁现象不仅浪费大量的系统资源,甚至导致整个系统崩溃,带来灾难性后果。所以,对于死锁问题在理论上和技术上都必须予以高度重视。
银行家算法一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产。银行家就像一个操作系统,客户就像运行的进程,银行家的资金就是系统的资源。
银行家算法需要确保以下四点:
1、当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
2、顾客可以分期贷款,但贷款的总数不能超过最大需求量;
3、当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
4、当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金。
清单1.银行家算法实现/*一共有5个进程需要请求资源,有3类资源*/publicclassBankDemo{//每个进程所需要的最大资源数publicstaticintMAX[][]={{7,5,3},{3,2,2},{9,0,2},{2,2,2},{4,3,3}};//系统拥有的初始资源数publicstaticintAVAILABLE[]={10,5,7};//系统已给每个进程分配的资源数publicstaticintALLOCATION[][]={{0,0,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0}};//每个进程还需要的资源数publicstaticintNEED[][]={{7,5,3},{3,2,2},{9,0,2},{2,2,2},{4,3,3}};//每次申请的资源数publicstaticintRequest[]={0,0,0};//进程数与资源数publicstaticintM=5,N=3;intFALSE=0;intTRUE=1;publicvoidshowdata(){inti,j;System.out.print("系统可用的资源数为:/n");for(j=0;jN;j++){System.out.print("资源"+j+":"+AVAILABLE[j]+"");}System.out.println();System.out.println("各进程还需要的资源量:");for(i=0;iM;i++){System.out.print("进程"+i+":");for(j=0;jN;j++){System.out.print("资源"+j+":"+NEED[i][j]+"");}System.out.print("/n");}System.out.print("各进程已经得到的资源量:/n");for(i=0;iM;i++){System.out.print("进程");System.out.print(i);for(j=0;jN;j++){System.out.print("资源"+j+":"+ALLOCATION[i][j]+"");}System.out.print("/n");}}//分配资源,并重新更新各种状态publicvoidchangdata(intk){intj;for(j=0;jN;j++){AVAILABLE[j]=AVAILABLE[j]-Request[j];ALLOCATION[k][j]=ALLOCATION[k][j]+Request[j];NEED[k][j]=NEED[k][j]-Request[j];}};//回收资源,并重新更新各种状态publicvoidrstordata(intk){intj;for(j=0;jN;j++){AVAILABLE[j]=AVAILABLE[j]+Request[j];ALLOCATION[k][j]=ALLOCATION[k][j]-Request[j];NEED[k][j]=NEED[k][j]+Request[j];}};//释放资源publicvoidfree(intk){for(intj=0;jN;j++){AVAILABLE[j]=AVAILABLE[j]+ALLOCATION[k][j];System.out.print("释放"+k+"号进程的"+j+"资源!/n");}}publicintcheck0(intk){intj,n=0;for(j=0;jN;j++){if(NEED[k][j]==0)n++;}if(n==3)return1;elsereturn0;}//检查安全性函数//所以银行家算法其核心是:保证银行家系统的资源数至少不小于一个客户的所需要的资源数。在安全性检查函数chkerr()上由这个方法来实现//这个循环来进行核心判断,从而完成了银行家算法的安全性检查工作。publicintchkerr(ints){intWORK;intFINISH[]=newint[M],temp[]=newint[M];//保存临时的安全进程序列inti,j,k=0;for(i=0;iM;i++)FINISH[i]=FALSE;for(j=0;jN;j++){WORK=AVAILABLE[j];//第j个资源可用数i=s;//判断第i个进程是否满足条件while(iM){if(FINISH[i]==FALSENEED[i][j]=WORK){WORK=WORK+ALLOCATION[i][j];FINISH[i]=TRUE;temp[k]=i;k++;i=0;}else{i++;}}for(i=0;iM;i++)if(FINISH[i]==FALSE){System.out.print("/n系统不安全!!!本次资源申请不成功!/n");return1;}}System.out.print("/n经安全性检查,系统安全,本次分配成功。/n");System.out.print("本次安全序列:");for(i=0;iM-1;i++){System.out.print("进程"+temp[i]+"-");}System.out.print("进程"+temp[M-1]);System.out.println("/n");return0;}}死锁示例
死锁问题是多线程特有的问题,它可以被认为是线程间切换消耗系统性能的一种极端情况。在死锁时,线程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是系统任务永远无法执行完成。死锁问题是在多线程开发中应该坚决避免和杜绝的问题。
一般来说,要出现死锁问题需要满足以下条件:
1、互斥条件:一个资源每次只能被一个线程使用。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
只要破坏死锁4个必要条件之一中的任何一个,死锁问题就能被解决。
我们先来看一个示例,前面说过,死锁是两个甚至多个线程被永久阻塞时的一种运行局面,这种局面的生成伴随着至少两个线程和两个或者多个资源。代码清单2所示的示例中,我们编写了一个简单的程序,它将会引起死锁发生,然后我们就会明白如何分析它。
清单2.死锁示例publicclassThreadDeadlock{publicstaticvoidmain(String[]args)throwsInterruptedException{Objectobj1=newObject();Objectobj2=newObject();Objectobj3=newObject();Threadt1=newThread(newSyncThread(obj1,obj2),"t1");Threadt2=newThread(newSyncThread(obj2,obj3),"t2");Threadt3=newThread(newSyncThread(obj3,obj1),"t3");t1.start();Thread.sleep();t2.start();Thread.sleep();t3.start();}}classSyncThreadimplementsRunnable{privateObjectobj1;privateObjectobj2;publicSyncThread(Objecto1,Objecto2){this.obj1=o1;this.obj2=o2;}
Overridepublicvoidrun(){Stringname=Thread.currentThread().getName();System.out.println(name+"acquiringlockon"+obj1);synchronized(obj1){System.out.println(name+"acquiredlockon"+obj1);work();System.out.println(name+"acquiringlockon"+obj2);synchronized(obj2){System.out.println(name+"acquiredlockon"+obj2);work();}System.out.println(name+"releasedlockon"+obj2);}System.out.println(name+"releasedlockon"+obj1);System.out.println(name+"finishedexecution.");}privatevoidwork(){try{Thread.sleep();}catch(InterruptedExceptione){e.printStackTrace();}}}在上面的程序中同步线程正完成Runnable的接口,它工作的是两个对象,这两个对象向对方寻求死锁而且都在使用同步阻塞。在主函数中,我使用了三个为同步线程运行的线程,而且在其中每个线程中都有一个可共享的资源。这些线程以向第一个对象获取封锁这种方式运行。但是当它试着向第二个对象获取封锁时,它就会进入等待状态,因为它已经被另一个线程封锁住了。这样,在线程引起死锁的过程中,就形成了一个依赖于资源的循环。当我执行上面的程序时,就产生了输出,但是程序却因为死锁无法停止。输出如清单3所示。
清单3.清单2运行输出t1acquiringlockonjava.lang.Object
1ddt1acquiredlockonjava.lang.Object1ddt2acquiringlockonjava.lang.Objectcb9t2acquiredlockonjava.lang.Objectcb9t3acquiringlockonjava.lang.Object1aa9f99t3acquiredlockonjava.lang.Object1aa9f99t1acquiringlockonjava.lang.Objectcb9t2acquiringlockonjava.lang.Object1aa9f99在此我们可以清楚地在输出结果中辨认出死锁局面,但是在我们实际所用的应用中,发现死锁并将它排除是非常难的。
死锁情况诊断JVM提供了一些工具可以来帮助诊断死锁的发生,如下面程序清单4所示,我们实现了一个死锁,然后尝试通过jstack命令追踪、分析死锁发生。
清单4.死锁代码importjava.util.concurrent.locks.ReentrantLock;//下面演示一个简单的死锁,两个线程分别占用south锁和north锁,并同时请求对方占用的锁,导致死锁publicclassDeadLockextendsThread{protectedObjectmyDirect;staticReentrantLocksouth=newReentrantLock();staticReentrantLocknorth=newReentrantLock();publicDeadLock(Objectobj){this.myDirect=obj;if(myDirect==south){this.setName("south");}else{this.setName("north");}}
Overridepublicvoidrun(){if(myDirect==south){try{north.lockInterruptibly();//占用northtry{Thread.sleep();}catch(Exceptionex){ex.printStackTrace();}south.lockInterruptibly();System.out.println("cartosouthhaspassed");}catch(InterruptedExceptionex){System.out.println("cartosouthiskilled");ex.printStackTrace();}finally{if(north.isHeldByCurrentThread()){north.unlock();}if(south.isHeldByCurrentThread()){south.unlock();}}}if(myDirect==north){try{south.lockInterruptibly();//占用southtry{Thread.sleep();}catch(Exceptionex){ex.printStackTrace();}north.lockInterruptibly();System.out.println("cartonorthhaspassed");}catch(InterruptedExceptionex){System.out.println("cartonorthiskilled");ex.printStackTrace();}finally{if(north.isHeldByCurrentThread()){north.unlock();}if(south.isHeldByCurrentThread()){south.unlock();}}}}publicstaticvoidmain(String[]args)throwsInterruptedException{DeadLockcar2south=newDeadLock(south);DeadLockcar2north=newDeadLock(north);car2south.start();car2north.start();}}jstack可用于导出Java应用程序的线程堆栈,-l选项用于打印锁的附加信息。我们运行jstack命令,输出入清单5和6所示,其中清单5里面可以看到线程处于运行状态,代码中调用了拥有锁投票、定时锁等候和可中断锁等候等特性的ReentrantLock锁机制。清单6直接打印出出现死锁情况,报告north和sourth两个线程互相等待资源,出现了死锁。
清单5.jstack运行输出1清单6.jstack运行输出片段2FoundoneJava-leveldeadlock:============================="north":waitingforownablesynchronizer0xc7c8,(ajava.util.concurrent.locks.ReentrantLock$NonfairSync),whichisheldby"south""south":waitingforownablesynchronizer0xc,(ajava.util.concurrent.locks.ReentrantLock$NonfairSync),whichisheldby"north"Javastackinformationforthethreadslistedabove:==================================================="north":atsun.misc.Unsafe.park(NativeMethod)-parkingtowaitfor0xc7c8(ajava.util.concurrent.locks.ReentrantLock$NonfairSync)atjava.util.concurrent.locks.LockSupport.park(LockSupport.java:)atjava.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:)atjava.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:)atjava.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:)atjava.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:)atDeadLock.run(DeadLock.java:50)"south":atsun.misc.Unsafe.park(NativeMethod)-parkingtowaitfor0xc(ajava.util.concurrent.locks.ReentrantLock$NonfairSync)atjava.util.concurrent.locks.LockSupport.park(LockSupport.java:)atjava.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:)atjava.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:)atjava.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:)atjava.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:)atDeadLock.run(DeadLock.java:28)Found1deadlock.死锁解决方案
死锁是由四个必要条件导致的,所以一般来说,只要破坏这四个必要条件中的一个条件,死锁情况就应该不会发生。
1、如果想要打破互斥条件,我们需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件;
2、打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景;
3、进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低;
4、避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。
如果我们在死锁检查时发现了死锁情况,那么就要努力消除死锁,使系统从死锁状态中恢复过来。消除死锁的几种方式:
1、最简单、最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程;
2、撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素;
3、进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。
其实即便是商业产品,依然会有很多死锁情况的发生,例如MySQL数据库,它也经常容易出现死锁案例。
MySQL死锁情况解决方法假设我们用Showinnodbstatus检查引擎状态时发现了死锁情况,如清单7所示。
清单7.MySQL死锁WAITINGFORTHISLOCKTOBEGRANTED:RECORDLOCKSspaceid0pagenonbitsindex`KEY_TSKTASK_MONTIME2`oftable`dcnet_db/TSK_TASK`trxidlock_modeXlocksrecbutnotgapwaitingRecordlock,heapnoPHYSICALRECORD:n_fields3;武汉白癜风医院白癜风如何治疗最好