饑餓和公平
如果一個線程因為CPU時間全部被其他線程搶走而得不到CPU運行時間,這種狀態被稱之為“饑餓”。而該線程被“饑餓致死”正是因為它得不到CPU運行時間的機會。解決饑餓的方案被稱之為“公平性” – 即所有線程均能公平地獲得運行機會。
下麵是本文討論的主題:
1. Java中導致饑餓的原因:
- 高優先級線程吞噬所有的低優先級線程的CPU時間。
- 線程被永久堵塞在一個等待進入同步塊的狀態。
- 線程在等待一個本身也處於永久等待完成的對象(比如調用這個對象的wait方法)。
2. 在Java中實現公平性方案,需要:
- 使用鎖,而不是同步塊。
- 公平鎖。
- 注意性能方麵。
Java中導致饑餓的原因
在Java中,下麵三個常見的原因會導致線程饑餓:
- 高優先級線程吞噬所有的低優先級線程的CPU時間。
- 線程被永久堵塞在一個等待進入同步塊的狀態,因為其他線程總是能在它之前持續地對該同步塊進行訪問。
- 線程在等待一個本身(在其上調用wait())也處於永久等待完成的對象,因為其他線程總是被持續地獲得喚醒。
高優先級線程吞噬所有的低優先級線程的CPU時間
你能為每個線程設置獨自的線程優先級,優先級越高的線程獲得的CPU時間越多,線程優先級值設置在1到10之間,而這些優先級值所表示行為的準確解釋則依賴於你的應用運行平台。對大多數應用來說,你最好是不要改變其優先級值。
線程被永久堵塞在一個等待進入同步塊的狀態
Java的同步代碼區也是一個導致饑餓的因素。Java的同步代碼區對哪個線程允許進入的次序沒有任何保障。這就意味著理論上存在一個試圖進入該同 步區的線程處於被永久堵塞的風險,因為其他線程總是能持續地先於它獲得訪問,這即是“饑餓”問題,而一個線程被“饑餓致死”正是因為它得不到CPU運行時 間的機會。
線程在等待一個本身(在其上調用wait())也處於永久等待完成的對象
如果多個線程處在wait()方法執行上,而對其調用notify()不會保證哪一個線程會獲得喚醒,任何線程都有可能處於繼續等待的狀態。因此存在這樣一個風險:一個等待線程從來得不到喚醒,因為其他等待線程總是能被獲得喚醒。
在Java中實現公平性
雖Java不可能實現100%的公平性,我們依然可以通過同步結構在線程間實現公平性的提高。
首先來學習一段簡單的同步態代碼:
public class Synchronizer{ public synchronized void doSynchronized(){ //do a lot of work which takes a long time } }
如果有一個以上的線程調用doSynchronized()方法,在第一個獲得訪問的線程未完成前,其他線程將一直處於阻塞狀態,而且在這種多線程被阻塞的場景下,接下來將是哪個線程獲得訪問是沒有保障的。
使用鎖方式替代同步塊
為了提高等待線程的公平性,我們使用鎖方式來替代同步塊。
public class Synchronizer{ Lock lock = new Lock(); public void doSynchronized() throws InterruptedException{ this.lock.lock(); //critical section, do a lot of work which takes a long time this.lock.unlock(); } }
注意到doSynchronized()不再聲明為synchronized,而是用lock.lock()和lock.unlock()來替代。
下麵是用Lock類做的一個實現:
public class Lock{ private boolean isLocked = false; private Thread lockingThread = null; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } 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; notify(); } }
注意到上麵對Lock的實現,如果存在多線程並發訪問lock(),這些線程將阻塞在對lock()方法的訪問上。另外,如果鎖已經鎖上(校對注: 這裏指的是isLocked等於true時),這些線程將阻塞在while(isLocked)循環的wait()調用裏麵。要記住的是,當線程正在等待 進入lock() 時,可以調用wait()釋放其鎖實例對應的同步鎖,使得其他多個線程可以進入lock()方法,並調用wait()方法。
這回看下doSynchronized(),你會注意到在lock()和unlock()之間的注釋:在這兩個調用之間的代碼將運行很長一段時間。 進一步設想,這段代碼將長時間運行,和進入lock()並調用wait()來比較的話。這意味著大部分時間用在等待進入鎖和進入臨界區的過程是用在 wait()的等待中,而不是被阻塞在試圖進入lock()方法中。
在早些時候提到過,同步塊不會對等待進入的多個線程誰能獲得訪問做任何保障,同樣當調用notify()時,wait()也不會做保障一定能喚醒線程(至於為什麼,請看線程通信)。因此這個版本的Lock類和doSynchronized()那個版本就保障公平性而言,沒有任何區別。
但我們能改變這種情況。當前的Lock類版本調用自己的wait()方法,如果每個線程在不同的對象上調用wait(),那麼隻有一個線程會在該對象上調用wait(),Lock類可以決定哪個對象能對其調用notify(),因此能做到有效的選擇喚醒哪個線程。
公平鎖
下麵來講述將上麵Lock類轉變為公平鎖FairLock。你會注意到新的實現和之前的Lock類中的同步和wait()/notify()稍有不同。
準確地說如何從之前的Lock類做到公平鎖的設計是一個漸進設計的過程,每一步都是在解決上一步的問題而前進的:Nested Monitor Lockout, Slipped Conditions和Missed Signals。這些本身的討論雖已超出本文的範圍,但其中每一步的內容都將會專題進行討論。重要的是,每一個調用lock()的線程都會進入一個隊列, 當解鎖後,隻有隊列裏的第一個線程被允許鎖住Farlock實例,所有其它的線程都將處於等待狀態,直到他們處於隊列頭部。
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(); boolean isLockedForThisThread = true; synchronized(this){ waitingThreads.add(queueObject); } while(isLockedForThisThread){ synchronized(this){ isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject; if(!isLockedForThisThread){ isLocked = true; waitingThreads.remove(queueObject); lockingThread = Thread.currentThread(); return; } } try{ queueObject.doWait(); }catch(InterruptedException e){ synchronized(this) { waitingThreads.remove(queueObject); } throw e; } } } 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){ waitingThreads.get(0).doNotify(); } } }
public class QueueObject { private boolean isNotified = false; public synchronized void doWait() throws InterruptedException { while(!isNotified){ this.wait(); } this.isNotified = false; } public synchronized void doNotify() { this.isNotified = true; this.notify(); } public boolean equals(Object o) { return this == o; } }
首先注意到lock()方法不在聲明為synchronized,取而代之的是對必需同步的代碼,在synchronized中進行嵌套。
FairLock新創建了一個QueueObject的實例,並對每個調用lock()的線程進行入隊列。調用unlock()的線程將從隊列頭部 獲取QueueObject,並對其調用doNotify(),以喚醒在該對象上等待的線程。通過這種方式,在同一時間僅有一個等待線程獲得喚醒,而不是 所有的等待線程。這也是實現FairLock公平性的核心所在。
請注意,在同一個同步塊中,鎖狀態依然被檢查和設置,以避免出現滑漏條件。
還需注意到,QueueObject實際是一個semaphore。doWait()和doNotify()方法在QueueObject中保存著 信號。這樣做以避免一個線程在調用queueObject.doWait()之前被另一個調用unlock()並隨之調用 queueObject.doNotify()的線程重入,從而導致信號丟失。queueObject.doWait()調用放置在 synchronized(this)塊之外,以避免被monitor嵌套鎖死,所以另外的線程可以解鎖,隻要當沒有線程在lock方法的 synchronized(this)塊中執行即可。
最後,注意到queueObject.doWait()在try – catch塊中是怎樣調用的。在InterruptedException拋出的情況下,線程得以離開lock(),並需讓它從隊列中移除。
性能考慮
如果比較Lock和FairLock類,你會注意到在FairLock類中lock()和unlock()還有更多需要深入的地方。這些額外的代碼
會導致FairLock的同步機製實現比Lock要稍微慢些。究竟存在多少影響,還依賴於應用在FairLock臨界區執行的時長。執行時長越
大,FairLock帶來的負擔影響就越小,當然這也和代碼執行的頻繁度相關。
文章轉自 並發編程網-ifeve.com
最後更新:2017-05-22 20:03:45