滑倒的条件是什么?
滑倒的条件意味着,那个来自于特定的时间的一个线程检查一个特定的条件直到这个条件作用于它之上,但是这个条件已经被另外一个线程改变了以至于对于第一个线程是错误的。这里有一个简单的例子:
public class Lock { private boolean isLocked = true; public void lock(){ synchronized(this){ while(isLocked){ try{ this.wait(); } catch(InterruptedException e){ //do nothing, keep waiting } } } synchronized(this){ isLocked = true; } } public synchronized void unlock(){ isLocked = false; this.notify(); } }
注意这个lock方法是怎样包含了两个同步块的。第一个同步块等待直到isLocked为false。第二个同步块设置isLocked为true,去锁住这个lock示例对于其他的线程。
想象下这个isLocked是false,并且两个线程同时调用lock方法。如果第一个线程进入到第一个同步块中,在第一个同步块中就会被抢占,这个线程将会检查isLocked并且注意到它是false。如果第二个线程现在被允许执行的,以及因此进入到第一个同步块中,这个线程将也会看到isLocked是false。现在两个线程已经读到这个条件是false。然后两个线程将会进入到第二个同步块中,设置isLocked为true并且继续。
这个场景是一个滑倒的条件的一个例子。两个线程检查这个条件,然后退出这个同步块,因此允许其他的线程去检查这个条件,对于随后的线程前面的两个线程改变条件之前。换句话说,这个条件对于那段时间已经滑倒了,这个条件被检查直到这个线程改变它对于随后的线程。
为了避免滑倒的条件,这个条件的检查和设置必须通过正在执行它的线程自动的去做,意味着没有其他的线程可以检查这个条件在第一个线程的条件的检查和设置之间。
在上面的例子的解决方案是简单的。只是移动这个行:isLocked=ture;到第一个同步块中,正好在while循环之后。就像下面这样:
public class Lock { private boolean isLocked = true; public void lock(){ synchronized(this){ while(isLocked){ try{ this.wait(); } catch(InterruptedException e){ //do nothing, keep waiting } } isLocked = true; } } public synchronized void unlock(){ isLocked = false; this.notify(); } }
一个更现实的例子
你可能会辩论你将不会像上面那样去实现一个锁,并且因此声明滑倒的条件只是理论上的问题。但是第一个例子被保持的简单的表达滑倒的条件的概念。
一个更现实的例子将会在一个公平锁实现期间出现,正如在饥饿和公平的文章中讨论的。如果我们看到来自于嵌套的监视器锁死的文章中这个幼稚的实现,并且尝试着移除这个嵌套的监视器锁问题,它是简单的去达到一个患有滑倒的条件问题的实现。首先,我将会显示来自于嵌套监视器锁死文章的那个例子:
//Fair Lock implementation with nested monitor lockout problem public class FairLock { private boolean isLocked = false; private Thread lockingThread = null; private List<QueueObject> waitingThreads = new ArrayList<QueueObject>(); public void lock() throws InterruptedException{ QueueObject queueObject = new QueueObject(); synchronized(this){ waitingThreads.add(queueObject); while(isLocked || waitingThreads.get(0) != queueObject){ synchronized(queueObject){ try{ queueObject.wait(); }catch(InterruptedException e){ waitingThreads.remove(queueObject); throw e; } } } waitingThreads.remove(queueObject); isLocked = true; lockingThread = Thread.currentThread(); } } public synchronized void unlock(){ if(this.lockingThread != Thread.currentThread()){ throw new IllegalMonitorStateException( "Calling thread has not locked this lock"); } isLocked = false; lockingThread = null; if(waitingThreads.size() > 0){ QueueObject queueObject = waitingThread.get(0); synchronized(queueObject){ queueObject.notify(); } } } }
public class QueueObject {}
注意,伴随着这个queueObject.wait()调用的synchronized(queueObject)是被嵌套在synchronized(this)块的内部的,导致这个嵌套的监视器闭锁问题。为了避免这个问题,这个synchronized(queueObject)块必须被移动到synchronized(this)块的外部。就像下面这样:
//Fair Lock implementation with slipped conditions problem public class FairLock { private boolean isLocked = false; private Thread lockingThread = null; private List<QueueObject> waitingThreads = new ArrayList<QueueObject>(); public void lock() throws InterruptedException{ QueueObject queueObject = new QueueObject(); synchronized(this){ waitingThreads.add(queueObject); } boolean mustWait = true; while(mustWait){ synchronized(this){ mustWait = isLocked } synchronized(queueObject){ if(mustWait){ try{ queueObject.wait(); }catch(InterruptedException e){ waitingThreads.remove(queueObject); throw e; } } } } synchronized(this){ waitingThreads.remove(queueObject); isLocked = true; lockingThread = Thread.currentThread(); } } }
注意:只有lock方法被显示了,因为它只是我已经改变的一个方法。
注意这个lock方法现在含有3个同步块。
第一个synchronized(this)块检查这个条件通过设置mustWait = isLocked || waitingThreads.get(0) != queueObject检查这个条件。
第二个synchronized(queueObject)块检查这个线程是否去等待。在这个时间可能另外一个线程已经释放这个锁了,但是让我们忘记这个时间。让我们假设这个锁已经被释放了,以至于这个线程会立刻退出这个synchronized(queueObject)块。
第三个块只是执行了如果mustWait = false。这个设置这个条件isLocked为true等等。并且离开这个lock方法。
想象下当这个锁被释放的时候,如果两个线程同时调用lock方法将会发生什么?首先线程1将会检查这个isLocked条件并且看到他是false。然后线程2将会做相同的事情。然后他们中没有一个将会等待,并且这两个都会设置这个状态isLocked为true。这个是滑倒的条件的最好的例子。
移除这个滑倒的条件的问题
为了移除上面例子中的滑倒的条件的问题,最后的synchronized(this)块的内容必须移动到第一个块中。这个代码将会自然而然的不得不也改变一点点,去适配这个移动。就像下面这样:
//Fair Lock implementation without nested monitor lockout problem, //but with missed signals problem. public class FairLock { private boolean isLocked = false; private Thread lockingThread = null; private List<QueueObject> waitingThreads = new ArrayList<QueueObject>(); public void lock() throws InterruptedException{ QueueObject queueObject = new QueueObject(); synchronized(this){ waitingThreads.add(queueObject); } boolean mustWait = true; while(mustWait){ synchronized(this){ mustWait = isLocked || waitingThreads.get(0) != queueObject; if(!mustWait){ waitingThreads.remove(queueObject); isLocked = true; lockingThread = Thread.currentThread(); return; } } synchronized(queueObject){ if(mustWait){ try{ queueObject.wait(); }catch(InterruptedException e){ waitingThreads.remove(queueObject); throw e; } } } } } }
注意这个本地变量mustWait是怎么样被检查和设置的在相同的同步代码块中。也要注意,甚至mustWait本地变量在synchronized(this)代码块的外部也会被检查,在while(mustWait)子句中,这个mustWait变量的值不会在synchronized(this)的外部被改变。一个线程认为mustWait为false也将会自动设置这个内部的条件(isLocked)以至于任何其他的线程检查这个条件将会认为是true。
这个return;语句在synchronized(this)块中是不需要的。它只是一个小的优化。如果这个线程不用等待(mustWait == false),然后这里就没有理由去进入到synchronized(queueObject)块中去执行这个if(mustWait)子句。
善于观察的读者将会注意到上面的一个公平锁的实现仍然会有一个丢失的信号问题。想象下这个FairLock示例当一个线程调用lock方法的时候被锁定了。第一个synchronized(this)块之后mustWait为true。然后想象下正在调用lock方法的这个线程被先占有了,并且锁住这个锁的线程调用unlock方法。如果你看到最早实现的unlock方法,你将会注意到它调用queueObject.notify()。但是,因为正在lock方法中等待的这个线程还仍然没有调用queueObject.wait()方法,对于queueObject.notify()方法的调用进入遗忘的区域。这个信号就丢失了。当调用lock方法的这个线程正好是在调用queueObject.wait()方法之后,它将会保持被锁的直到一些其他的线程调用unlock方法,这个可能永远不会发生。
丢失的信号问题的缘由是,在饥饿和公平文章中显示的FairLock实现,已经把QueueObject类转换为拥有两个方法的信号量:doWait方法和doNotify方法。这些方法存储和作用到QueueObject内部的信号。那种方式信号就不会丢失了。甚至如果doNotify方法在doWait方法之前调用。
翻译地址:http://tutorials.jenkov.com/java-concurrency/slipped-conditions.html