作者|王磊来源|Java中文社区(ID:javacn666)请联系授权(微信ID:GG_Stone)除了重启程序,还可以考虑使用顺序锁和轮询锁。这部分内容可以参考我之前的文章,这里不再赘述。但是在轮询锁的使用过程中,如果使用不当会带来新的严重的问题,那么这篇文章我们就来看看这些问题以及相应的解决方法。问题演示在我们使用轮询锁之前,可能会出现这样的问题:/创建锁ALocklockB=newReentrantLock();//创建锁B//创建线程1Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){lockA.lock();//锁System.out.println("线程1:获取锁A!");try{Thread.sleep(1000);System.out.println("Thread1:WaitingtogetB...");lockB.lock();//锁try{System.out.println("Thread1:GetlockB!");}finally{lockA.unlock();//释放锁}}catch(InterruptedExceptione){e.printStackTrace();}finally{lockA.unlock();//释放锁}}});t1.start();//运行线程//创建线程2Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){lockB.lock();//锁定System.out.println("线程2:获取锁B!");try{Thread.sleep(1000);System.out.println("线程2:等待获取A...");lockA.lock();//Locktry{System.out.println("Thread2:GetlockA!");}finally{lockA.unlock();//释放锁}}catch(InterruptedExceptione){e.printStackTrace();}finally{lockB.unlock();//释放锁}}});t2.start();//运行线程}}上面代码的执行结果如下:从上面结果,我们可以看到,此时在程序中,线程相互等待,试图获取对方的(锁)资源。这是一个典型的死锁问题。简单的轮询锁当死锁问题出现时,我们可以使用轮询锁来解决。它的实现思路是通过轮询的方式获取多个锁。如果中途有任何锁获取失败,则执行回滚操作,释放当前线程拥有的所有锁,等待下一次重新执行,从而防止多个线程同时拥有和占用锁资源,从而直接解决死锁问题。简单版的轮询锁实现如下:lockALocklockB=newReentrantLock();//创建锁B//创建线程1(使用轮询锁)Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){//调用轮询锁pollingLock(lockA,lockB);}});t1.start();//运行线程//创建线程2Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){lockB.lock();//LockSystem.out.println("Thread2:GetlockB!");try{Thread.sleep(1000);System.out.println("线程2:等待获取A...");lockA.lock();//加锁try{System.out.println("线程2:获取到锁A!");}finally{lockA.unlock();//释放锁}}catch(InterruptedExceptione){e.printStackTrace();}finally{lockB.unlock();//释放锁}}});t2.start();//运行线程}/***轮询锁*/privatestaticvoidpollingLock(LocklockA,LocklockB){//轮询锁while(true){if(lockA.tryLock()){//尝试获取锁System.out.println("线程1:获取锁A!");try{Thread.sleep(1000);System.out.println("Thread1:WaitingtoacquireB...");if(lockB.tryLock()){//尝试获取锁try{System.out.println("Thread1:ObtainedlockB!");}finally{lockB.unlock();//释放锁System.out.println("Thread1:ReleaselockB.");break;}}}catch(InterruptedExceptione){e.printStackTrace();}finally{lockA.unlock();//释放锁System.out.println("Thread1:ReleaselockA.");}}//等待一秒再继续try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}}}上面代码的执行结果如下:从上面的结果可以看出,当我们在程序中使用轮询锁时,不会出现死锁问题,但是上面的轮询锁并不完美,我们来看看这个轮询锁会出现什么样的问题?问题一:无限循环上面的简单版轮询锁,如果有线程一直在占用或者长期占用锁资源,就会导致这个轮询锁进入死循环状态,它会尝试一直去获取锁资源,会带来新的问题和不必要的性能开销。具体例子如下反例importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassSolveDeadLockExample{publicstaticvoidmain(String[]args){LocklockA=newReentrantLock();//创建锁ALocklockB=newReentrantLock();//createLockB//创建线程1(使用轮询锁)Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){//调用轮询锁pollingLock(lockA,lockB);}});t1.start();//Runthread//创建线程2Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){lockB.lock();//锁定System.out.println("线程2:获取锁B!");try{Thread.sleep(1000);System.out.println("线程2:等待获取A...");lockA.lock();//锁try{System.out.println("线程2:获取锁A!");}finally{lockA.unlock();//释放锁}}catch(InterruptedExceptione){e.printStackTrace();}finally{//如果这里的代码没有执行,说明线程2还没有释放锁Resource//lockB.unlock();}}});t2.start();//运行线程}/***轮询锁*/publicstaticvoidpollingLock(LocklockA,LlocklockB){while(true){if(lockA.tryLock()){//尝试获取锁System.out.println("Thread1:GetlockA!");试试{Thread.sleep(1000);System.out.println("线程1:等待获取B...");if(lockB.tryLock()){//尝试获取锁try{System.out.println("Thread1:ObtainedlockB!");}finally{lockB.unlock();//ReleaseLockSystem.out.println("Thread1:ReleaselockB.");break;}}}catch(InterruptedExceptione){e.printStackTrace();}finally{lockA.unlock();//释放系统锁.out.println("Thread1:ReleaselockA.");}}//等待一秒后继续执行try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}}}上面代码的执行结果如下:从上面的结果可以看出,线程1的轮询锁进入了死循环状态。n次尝试获取锁后,如果没有获取到锁,则认为获取锁失败,执行失败策略后终止轮询(失败策略可以是日志记录或其他操作);增加一个最大时间限制:如果n秒后获取到锁,如果还没有获取到锁,则认为获取锁失败,执行失败策略后终止轮询。选择上述策略之一可以解决死循环问题。为了实现成本,我们可以使用轮询最大次数的方法来提高轮询锁。具体实现代码如下:importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassSolveDeadLockExample{publicstaticvoidmain(String[]args){LocklockA=newReentrantLock();//创建锁ALocklockB=newReentrantLock();//创建锁B//创建线程1(使用轮询锁)Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){//调用轮询锁pollingLock(lockA,lockB,3);}});t1.start();//运行线程//创建线程2Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){lockB.lock();//lockSystem.out.println("线程2:获取锁B!");try{Thread.sleep(1000);System.out.println("线程2:等待获取A...");lockA.lock();//加锁try{System.out.println("线程2:获得锁A!");}finally{lockA.unlock();//释放锁}}catch(InterruptedExceptione){e.printStackTrace();}finally{//线程2忘记释放锁资源//lockB.unlock();//释放锁}}});t2.start();//runningthread}/***轮询锁**maxCount:最大轮询次数*/publicstaticvoidpollingLock(LocklockA,LocklockB,intmaxCount){//轮询次数计数器intcount=0;while(true){if(lockA.tryLock()){//尝试获取锁Ssystem.out.println("线程1:获取锁A!");试试{Thread.sleep(1000);System.out.println("线程1:等待获取B...");if(lockB.tryLock()){//尝试获取锁try{System.out.println("Thread1:GetlockB!");}finally{lockB.unlock();//释放系统锁.out.println("Thread1:ReleaselockB.");break;}}}catch(InterruptedExceptione){e.printStackTrace();}finally{lockA.unlock();//释放锁System.out.println("Thread1:releaseLockA.");}}//判断是否超过最大次数if(count++>maxCount){//终止循环System.out.println("轮询锁获取失败,logorexecuteotherfailurestrategies");return;}//等待一秒后再继续尝试获取锁try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}}}上面代码的执行结果如下:从上面的结果可以看出,当我们改进后,轮询锁就不会出现死循环的问题了。它会在一定次数后尝试终止执行。问题2:线程饿死。上面轮询锁的轮询等待时间是一个固定的时间。如下代码所示://等待1s,然后尝试获取(轮询)锁try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}这会导致线程特殊情况下饿死的问题,也就是轮询锁一直获取不到锁的问题,比如下面这个例子。反例importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassSolveDeadLockExample{publicstaticvoidmain(String[]args){LocklockA=newReentrantLock();//创建锁ALocklockB=newReentrantLock();//createLockB//创建线程1(使用轮询锁)Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){//调用轮询锁pollingLock(lockA,lockB,3);}});t1.start();//运行线程//创建线程2Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){while(true){lockB.lock();//加锁System.out.println("线程2:获取锁B!");try{System.out.println("线程2:等待获取A...");lockA.lock();//加锁try{System.out.println("线程2:获取锁A!");}finally{lockA.unlock();//释放锁}}finally{lockB.unlock();//释放锁}//等待一秒继续执行try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}}});t2.start();//运行线程}/***轮询锁*/publicstaticvoidpollingLock(LocklockA,LocklockB,intmaxCount){//循环计数计数器intcount=0;while(true){if(lockA.tryLock()){//尝试获取锁System.out.println("Thread1:GetlockA!");试试{Thread.sleep(100);//等待0.1s(获取锁所需时间)System.out.println("线程1:等待获取B...");if(lockB.tryLock()){//Try获取锁try{System.out.println("Thread1:GetlockB!");}finally{lockB.unlock();//释放锁System.out.println("Thread1:ReleaselockB。“);休息;}}}catch(InterruptedExceptione){e.printStackTrace();}finally{lockA.unlock();//释放锁System.out.println("Thread1:ReleaselockA.");}}//判断是否超过最大次数if(count++>maxCount){//终止循环System.out.println("轮询锁获取失败,记录或执行其他失败策略");return;}//等待一个secondbeforecontinuetotry获取锁try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}}}上面代码的执行结果如下:从上面的结果来看,它可以看出线程1(轮询锁)一直没有得到这个结果的原因是线程1每次轮询的等待时间是固定的1s,线程2也是以同样的频率每1s获取一次锁,这样会造成线程2总是先等待。成功拿到了锁,但是线程1会一直处于“饿死”的状态。执行流程如下图所示:优化版接下来我们可以将轮询锁的固定等待时间提高为固定时间+随机时间这样就解决了同频的轮询锁“饿死”的问题可以避免获取锁。具体实现代码如下:importjava.util.Random;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassSolveDeadLockExample{privatestaticRandomrdm=newRandom();publicstaticvoidmain(String[]args){LocklockA=newReentrantLock();//创建锁ALocklockB=newReentrantLock();//创建锁B//创建线程1(使用轮询锁)Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){//调用轮询锁pollingLock(lockA,lockB,3);}});t1.start();//运行线程//创建线程2Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){while(true){lockB.lock();//lockSystem.out.println("线程2:获取锁B!");try{System.out.println("线程2:等待获取A...");lockA.lock();//加锁try{System.out.println("Thread2:gotlockA!");}finally{lockA.unlock();//释放锁}}finally{lockB.unlock();//释放锁}//等待一秒继续执行try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}}});t2.start();//运行线程}/***轮询锁*/publicstaticvoidpollingLock(LocklockA,LocklockB,intmaxCount){//循环计数器intcount=0;while(true){if(lockA.tryLock()){//尝试获取锁System.out.println("Thread1:获得锁A!");try{Thread.sleep(100);//等待0.1s(获取锁所需时间)System.out.println("Thread1:WaitingtoacquireB...");if(lockB.tryLock()){//尝试获取锁try{System.out.println("线程1:获取锁B!");}finally{lockB.unlock();//释放锁System.out.println("线程1:释放锁B。");break;}}}catch(InterruptedExceptione){e.printStackTrace();}finally{lockA.unlock();//释放锁System.out.println("Thread1:ReleaselockA.");}}//判断是否超过最大次数if(count++>maxCount){//终止循环System.out.println("轮询锁获取失败,记录或执行其他失败策略");return;}//等待一定时间(固定时间+随机时间)然后继续尝试获取锁try{Thread.sleep(300+rdm.nextInt(8)*100);//固定时间+随机time}catch(InterruptedExceptione){e.printStackTrace();}}}}上面代码的执行结果如下:从上面的结果可以看出线程1(轮询锁)加入随机等待时间后,不会出现线程饥饿的问题。解决了死锁问题,但是简单版的轮询锁在某些情况下会出现死循环和线程饥饿问题,所以我们对轮询锁进行了优化,增加了轮询锁的最大轮询次数,以及随机轮询等待时间,从而解决引入轮询锁带来的新问题,从而可以愉快的使用它来解决死锁问题。
