所謂 Slipped conditions,就是說, 從一個線程檢查某一特定條件到該線程操作此條件期間,這個條件已經(jīng)被其它線程改變,導致第一個線程在該條件上執(zhí)行了錯誤的操作。這里有一個簡單的例子:
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()方法包含了兩個同步塊。第一個同步塊執(zhí)行 wait 操作直到 isLocked 變?yōu)?false 才退出,第二個同步塊將 isLocked 置為 true,以此來鎖住這個 Lock 實例避免其它線程通過 lock()方法。
我們可以設(shè)想一下,假如在某個時刻 isLocked 為 false, 這個時候,有兩個線程同時訪問 lock 方法。如果第一個線程先進入第一個同步塊,這個時候它會發(fā)現(xiàn) isLocked 為 false,若此時允許第二個線程執(zhí)行,它也進入第一個同步塊,同樣發(fā)現(xiàn) isLocked 是 false?,F(xiàn)在兩個線程都檢查了這個條件為 false,然后它們都會繼續(xù)進入第二個同步塊中并設(shè)置 isLocked 為 true。
這個場景就是 slipped conditions 的例子,兩個線程檢查同一個條件, 然后退出同步塊,因此在這兩個線程改變條件之前,就允許其它線程來檢查這個條件。換句話說,條件被某個線程檢查到該條件被此線程改變期間,這個條件已經(jīng)被其它線程改變過了。
為避免 slipped conditions,條件的檢查與設(shè)置必須是原子的,也就是說,在第一個線程檢查和設(shè)置條件期間,不會有其它線程檢查這個條件。
解決上面問題的方法很簡單,只是簡單的把 isLocked = true 這行代碼移到第一個同步塊中,放在 while 循環(huán)后面即可:
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();
}
}
現(xiàn)在檢查和設(shè)置 isLocked 條件是在同一個同步塊中原子地執(zhí)行了。
也許你會說,我才不可能寫這么挫的代碼,還覺得 slipped conditions 是個相當理論的問題。但是第一個簡單的例子只是用來更好的展示 slipped conditions。
饑餓和公平中實現(xiàn)的公平鎖也許是個更現(xiàn)實的例子。再看下嵌套管程鎖死中那個幼稚的實現(xiàn),如果我們試圖解決其中的嵌套管程鎖死問題,很容易產(chǎn)生 slipped conditions 問題。 首先讓我們看下嵌套管程鎖死中的例子:
//Fair Lock implementation with nested monitor lockout problem
public class FairLock {
private boolean isLocked = false;
private Thread lockingThread = null;
private List waitingThreads =
new ArrayList();
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 {}
我們可以看到 synchronized(queueObject)及其中的 queueObject.wait()調(diào)用是嵌在 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 waitingThreads =
new ArrayList();
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()方法,這里只展現(xiàn)了 lock 方法。
現(xiàn)在 lock()方法包含了 3 個同步塊。
第一個,synchronized(this)塊通過 mustWait = isLocked || waitingThreads.get(0) != queueObject 檢查內(nèi)部變量的值。
第二個,synchronized(queueObject)塊檢查線程是否需要等待。也有可能其它線程在這個時候已經(jīng)解鎖了,但我們暫時不考慮這個問題。我們就假設(shè)這個鎖處在解鎖狀態(tài),所以線程會立馬退出 synchronized(queueObject)塊。
第三個,synchronized(this)塊只會在 mustWait 為 false 的時候執(zhí)行。它將 isLocked 重新設(shè)回 true,然后離開 lock()方法。
設(shè)想一下,在鎖處于解鎖狀態(tài)時,如果有兩個線程同時調(diào)用 lock()方法會發(fā)生什么。首先,線程 1 會檢查到 isLocked 為 false,然后線程 2 同樣檢查到 isLocked 為 false。接著,它們都不會等待,都會去設(shè)置 isLocked 為 true。這就是 slipped conditions 的一個最好的例子。
要解決上面例子中的 slipped conditions 問題,最后一個 synchronized(this)塊中的代碼必須向上移到第一個同步塊中。為適應(yīng)這種變動,代碼需要做點小改動。下面是改動過的代碼:
//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 waitingThreads =
new ArrayList();
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 變量從來沒有在 synchronized(this)同步塊外被賦值。當一個線程檢查到 mustWait 是 false 的時候,它將自動設(shè)置內(nèi)部的條件(isLocked),所以其它線程再來檢查這個條件的時候,它們就會發(fā)現(xiàn)這個條件的值現(xiàn)在為 true 了。
synchronized(this)塊中的 return;語句不是必須的。這只是個小小的優(yōu)化。如果一個線程肯定不會等待(即 mustWait 為 false),那么就沒必要讓它進入到 synchronized(queueObject)同步塊中和執(zhí)行 if(mustWait)子句了。
細心的讀者可能會注意到上面的公平鎖實現(xiàn)仍然有可能丟失信號。設(shè)想一下,當該 FairLock 實例處于鎖定狀態(tài)時,有個線程來調(diào)用 lock()方法。執(zhí)行完第一個 synchronized(this)塊后,mustWait 變量的值為 true。再設(shè)想一下調(diào)用 lock()的線程是通過搶占式的,擁有鎖的那個線程那個線程此時調(diào)用了 unlock()方法,但是看下之前的 unlock()的實現(xiàn)你會發(fā)現(xiàn),它調(diào)用了 queueObject.notify()。但是,因為 lock()中的線程還沒有來得及調(diào)用 queueObject.wait(),所以 queueObject.notify()調(diào)用也就沒有作用了,信號就丟失掉了。如果調(diào)用 lock()的線程在另一個線程調(diào)用 queueObject.notify()之后調(diào)用 queueObject.wait(),這個線程會一直阻塞到其它線程調(diào)用 unlock 方法為止,但這永遠也不會發(fā)生。
公平鎖實現(xiàn)的信號丟失問題在饑餓和公平一文中我們已有過討論,把 QueueObject 轉(zhuǎn)變成一個信號量,并提供兩個方法:doWait()和 doNotify()。這些方法會在 QueueObject 內(nèi)部對信號進行存儲和響應(yīng)。用這種方式,即使 doNotify()在 doWait()之前調(diào)用,信號也不會丟失。