詳解Java Slipped Conditions
所謂Slipped conditions,就是說, 從一個線程檢查某一特定條件到該線程操作此條件期間,這個條件已經被其它線程改變,導致第一個線程在該條件上執行瞭錯誤的操作。這裡有一個簡單的例子:
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()方法包含瞭兩個同步塊。第一個同步塊執行wait操作直到isLocked變為false才退出,第二個同步塊將isLocked置為true,以此來鎖住這個Lock實例避免其它線程通過lock()方法。
我們可以設想一下,假如在某個時刻isLocked為false, 這個時候,有兩個線程同時訪問lock方法。如果第一個線程先進入第一個同步塊,這個時候它會發現isLocked為false,若此時允許第二個線程執行,它也進入第一個同步塊,同樣發現isLocked是false。現在兩個線程都檢查瞭這個條件為false,然後它們都會繼續進入第二個同步塊中並設置isLocked為true。
這個場景就是slipped conditions的例子,兩個線程檢查同一個條件, 然後退出同步塊,因此在這兩個線程改變條件之前,就允許其它線程來檢查這個條件。換句話說,條件被某個線程檢查到該條件被此線程改變期間,這個條件已經被其它線程改變過瞭。
為避免slipped conditions,條件的檢查與設置必須是原子的,也就是說,在第一個線程檢查和設置條件期間,不會有其它線程檢查這個條件。
解決上面問題的方法很簡單,隻是簡單的把isLocked = true這行代碼移到第一個同步塊中,放在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(); } }
現在檢查和設置isLocked條件是在同一個同步塊中原子地執行瞭。
一個更現實的例子
也許你會說,我才不可能寫這麼挫的代碼,還覺得slipped conditions是個相當理論的問題。但是第一個簡單的例子隻是用來更好的展示slipped conditions。
饑餓和公平中實現的公平鎖也許是個更現實的例子。再看下嵌套管程鎖死中那個幼稚的實現,如果我們試圖解決其中的嵌套管程鎖死問題,很容易產生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(); } } } }1public class QueueObject {}
我們可以看到synchronized(queueObject)及其中的queueObject.wait()調用是嵌在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()方法,這裡隻展現瞭lock方法。
現在lock()方法包含瞭3個同步塊。
第一個,synchronized(this)塊通過mustWait = isLocked || waitingThreads.get(0) != queueObject檢查內部變量的值。
第二個,synchronized(queueObject)塊檢查線程是否需要等待。也有可能其它線程在這個時候已經解鎖瞭,但我們暫時不考慮這個問題。我們就假設這個鎖處在解鎖狀態,所以線程會立馬退出synchronized(queueObject)塊。
第三個,synchronized(this)塊隻會在mustWait為false的時候執行。它將isLocked重新設回true,然後離開lock()方法。
設想一下,在鎖處於解鎖狀態時,如果有兩個線程同時調用lock()方法會發生什麼。首先,線程1會檢查到isLocked為false,然後線程2同樣檢查到isLocked為false。接著,它們都不會等待,都會去設置isLocked為true。這就是slipped conditions的一個最好的例子。
解決Slipped Conditions問題
要解決上面例子中的slipped conditions問題,最後一個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 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的時候,它將自動設置內部的條件(isLocked),所以其它線程再來檢查這個條件的時候,它們就會發現這個條件的值現在為true瞭。
synchronized(this)塊中的return;語句不是必須的。這隻是個小小的優化。如果一個線程肯定不會等待(即mustWait為false),那麼就沒必要讓它進入到synchronized(queueObject)同步塊中和執行if(mustWait)子句瞭。
細心的讀者可能會註意到上面的公平鎖實現仍然有可能丟失信號。設想一下,當該FairLock實例處於鎖定狀態時,有個線程來調用lock()方法。執行完第一個 synchronized(this)塊後,mustWait變量的值為true。再設想一下調用lock()的線程是通過搶占式的,擁有鎖的那個線程那個線程此時調用瞭unlock()方法,但是看下之前的unlock()的實現你會發現,它調用瞭queueObject.notify()。但是,因為lock()中的線程還沒有來得及調用queueObject.wait(),所以queueObject.notify()調用也就沒有作用瞭,信號就丟失掉瞭。如果調用lock()的線程在另一個線程調用queueObject.notify()之後調用queueObject.wait(),這個線程會一直阻塞到其它線程調用unlock方法為止,但這永遠也不會發生。
公平鎖實現的信號丟失問題在饑餓和公平一文中我們已有過討論,把QueueObject轉變成一個信號量,並提供兩個方法:doWait()和doNotify()。這些方法會在QueueObject內部對信號進行存儲和響應。用這種方式,即使doNotify()在doWait()之前調用,信號也不會丟失。
以上就是詳解Java Slipped Conditions的詳細內容,更多關於Java Slipped Conditions的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 淺談Java並發之同步器設計
- 一篇文章讓你徹底瞭解Java可重入鎖和不可重入鎖
- 一文帶你搞懂Java中Synchronized和Lock的原理與使用
- Java如何正確的使用wait-notify方法你知道嗎
- Java的Synchronized關鍵字學習指南(全面 & 詳細)