滑倒条件
什么是滑倒条件(Slipped Condition)?
滑动条件意味着,从一个线程检查某个条件到对其执行操作之前,该条件已被另一个线程更改,以致第一个线程无法操作。这是一个简单的示例:
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"为假。第二个块将" isLocked"设置为true,以锁定其他线程的" Lock"实例。
想象一下," isLocked"为假,并且两个线程同时调用" lock()"。如果进入第一个同步块的第一个线程紧接在第一个同步块之后,则该线程将检查" isLocked"并指出它为假。如果现在允许第二个线程执行,从而进入第一个同步块,则该线程也将isLocked视为false。现在,两个线程都将条件读取为false。然后,两个线程都将进入第二个同步块,将" isLocked"设置为true,然后继续。
这种情况是打滑情况的一个例子。两个线程都测试条件,然后退出同步块,从而允许其他线程在两个第一个线程中的任何一个更改后续线程的条件之前测试条件。换句话说,该条件从检查条件之时起一直滑动,直到线程将其更改为后续线程为止。
为避免条件打滑,必须由执行该操作的线程以原子方式进行条件的测试和设置,这意味着没有其他线程可以在第一个线程进行的测试和条件设置之间检查条件。
上面示例中的解决方案很简单。只需在while循环之后,将" isLocked = true;"行上移到第一个同步块中即可。外观如下:
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(); } }
现在," isLocked"条件的测试和设置是从同一同步块内部自动完成的。
一个更现实的例子
我们可能会正确地辩称,我们将永远不会像本文中所示的第一个实现那样实现Lock,因此声称滑移条件是一个相当理论上的问题。但是第一个示例保持相当简单,以更好地传达滑移条件的概念。
一个更现实的例子是在实施公平锁定期间,如《饥饿与公平》一书中所讨论的。如果我们从"嵌套监视器锁定"文本中看过一个简单的实现,并尝试消除它的嵌套监视器锁定问题,那么很容易得出遭受滑移条件困扰的实现。首先,我将显示嵌套监视器锁定文本中的示例:
//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 || waitingThreads.get(0) != queueObject; } 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"时才执行第三个" synchronized(this)"块。这会将条件" isLocked"设置回" true"等,并保留" lock()"方法。
想象一下,如果两个线程在解锁时同时调用lock()
会发生什么。第一个线程1将检查isLocked
条件,并认为它为假。然后线程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"。还要注意,即使还在synchronized(this)
代码块之外检查了mustWait
局部变量,在while(mustWait)
子句中,mustWait
变量的值也不会在之外改变"。已同步(this)
。一个将" mustWait"评估为false的线程也会自动设置内部条件(" isLocked"),以便其他任何检查条件的线程都将其评估为true。
不必在synchronized(this)
块中使用return;
语句。这只是一个小的优化。如果线程不必等待(mustWait == false
),则没有理由进入synchronized(queueObject)
块并执行if(mustWait)
子句。
细心的读者会注意到,公平锁的上述实现仍然遭受信号丢失的困扰。想象一下,当线程调用lock()
时,FairLock实例被锁定。在第一个" synchronized(this)"块之后," mustWait"为真。然后想象一下,调用" lock()"的线程被抢占了,而锁定了锁的线程则调用了unlock()。如果查看前面显示的unlock()
实现,我们会注意到它调用了queueObject.notify()
。但是,由于等待在lock()中的线程尚未调用queueObject.wait(),因此对queueObject.notify()的调用被遗忘了。信号丢失。当线程在调用queueObject.wait()之后立即调用lock()时,它将一直处于阻塞状态,直到其他线程调用unlock()为止,这可能永远不会发生。
遗漏的信号问题是文本"饥饿与公平"中显示的" FairLock"实现将" QueueObject"类转换为带有两种方法的信号量的原因:doWait()和doNotify()。这些方法在QueueObject内部存储并响应信号。这样,即使在doWait()之前调用了doNotify(),信号也不会丢失。