原文地址: http://tutorials.jenkov.com/j...html
若是一個線程沒有被分配到 CPU 執行時間,該線程就處於「飢餓」狀態。若是老是分配不到 CPU 執行時間(由於老是被分配到其餘線程去了),那麼該線程可能會被「餓死」。有一種策略用於避免出現該問題,稱做「公平策略」,即保證全部的線程都能公平地獲得被執行的機會。java
在 Java 中,有三種最廣泛的情形會致使飢餓的發生:安全
synchronized
塊,以至某些線程老是得不到機會;wait()
方法)時,徹底得不到喚醒的機會,由於被喚醒的老是別的線程。每一個線程均可以單獨設置優先級。優先級越高,該線程就能得到更多的 CPU 執行時間。優先級的值最低爲 1 最高爲 10。至於如何根據優先級來分配 CPU 執行時間,則依賴於操做系統的具體實現。在大多數應用中,咱們最好不要去擅自修改它。this
Java 當中的 synchronized 代碼塊也是致使飢餓的一個因素。它不保證線程進入的順序,因此理論上某個線程可能永遠沒法進入 synchronized 塊,這種狀況下能夠說這個線程就被「餓死」了。操作系統
當多個線程同時調用的某個對象的 wait() 方法並等待時,notify() 方法不保證必定能喚醒哪一個指定的線程。因此若是它老是不去喚醒某個線程的話,這個線程就處於永久性地等待當中了。線程
固然咱們沒辦法實現 100% 的絕對公平,但仍是能夠經過一些結構上的設計來增長線程之間的公平性。設計
首先咱們來看一個簡單的 synchronized 代碼塊:code
public class Synchronizer{ public synchronized void doSynchronized(){ //do a lot of work which takes a long time } }
當多個線程調用 doSynchronized()
方法時,只有一個線程可以進入該方法並執行,並且該線程退出該方法後,正在等待的線程中沒法保證哪個纔是接下來能夠進入的。htm
爲了加強公平性,第一步咱們先把 synchronized 塊改成鎖對象:對象
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()
方法自己如今再也不是同步的了,須要同步執行的代碼如今由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(); } }
結合上面 Synchronizer 類和這裏的 Lock 實現,你會看到:首先,當多個線程調用 lock() 方法時,它們會被阻塞;其次,當 Lock 對象處於鎖住狀態時,進入 lock() 方法的線程會在 wait() 語句處阻塞。這裏要注意:當線程成功調用 wait() 方法時,會自動釋放 Lock 對象的鎖,因而其餘的線程可以得以進入 lock() 方法,最終會有多個線程都阻塞在 wait() 語句處。
咱們回頭看 doSynchronized() 方法中 lock() 和 unlock() 之間的部分,假設這部分代碼須要很長時間來執行,甚至比線程在 wait() 語句處等待所花的時間都長的多。那麼線程得到鎖所需的時間主要也是耗在 wait() 語句處,而不是進入 lock() 方法的時候。
在目前這個版本的代碼中,不論線程是在 synchronized 塊阻塞,仍是在 wait() 處阻塞,都不能保證哪一個線程能必定被喚醒,因此目前的代碼還沒有提供公平策略。
(譯註:之因此改爲這樣,目的是令線程在進入 lock() 方法時的阻塞時間儘量短,也就是全部的線程都在 wait() 處阻塞,以便實施接下來的改動。)
目前版本的 Lock 對象是在調用自身的 wait() 方法。咱們改掉這點,讓每一個線程調用不一樣對象的 wait() 方法的話,那麼就能夠自行挑選調用哪一個對象的 notify() 方法,以此實現自行挑選喚醒哪一個線程。
下面的代碼展現了將 Lock 類轉化爲 FairLock 類的結果。請注意同步方式和 wait()
/notify()
的調用方式有了哪些的變化。
整個改動的實現是階段性的,這個過程當中須要依次解決內部鎖對象死鎖、同步條件丟失以及解鎖信號丟失等問題。因爲篇幅長度所限這裏就不詳述了(請參考上面的連接)。這裏最重要的改動點,就是對 lock()
方法的調用如今是放在隊列中,全部的線程以隊列中的順序來依次得到 FairLock 對象的鎖。
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
。由於只有這個方法裏面的部分代碼才須要同步。
FairLock 會爲每一個線程建立一個新的 QueueObject
對象並將其加入隊列。調用 unlock()
方法的線程會從隊列中取第一個元素對象並調用它的 doNotify()
方法,這樣喚醒的就只有一個線程,而不是一堆線程。這個就是 FairLock 的公平機制所在。
注意接下來就是在同步塊中從新檢查條件並更新鎖狀態,這是爲了不同步條件丟失。
此外 QueueObject
其實是一個信號量,doWait()
和 doNotify()
方法的目的是存取鎖的狀態信號,以免解鎖信號丟失,即在一個線程調用 queueObject.doWait()
以前,另外一個線程已經在 unlock()
方法中調用了該對象的 queueObject.doNotify()
方法。至於將 queueObject.doWait()
方法的調用放在同步塊外面,是爲了不內部對象死鎖的狀況發生,這樣另外一個線程就能夠持有 FairLock 對象的鎖,並安全的調用 unlock()
方法了。
最後就是對 queueObject.doWait()
這條語句進行異常捕獲。若是這條語句執行時發生了 InterruptedException 異常,那麼就須要在離開這個方法前將 queueObject 對象從隊列中去掉。
咱們把 Lock
和 FairLock
對比一下就會看到後者的 lock()
和 unlock()
增長了不少代碼,它們會致使其執行效率比前者略有降低。這個影響的程度如何,取決於 lock()
和 unlock()
之間的同步代碼的執行時間,該時間越長,則影響就越小。固然同時也取決於鎖自己的使用頻繁程度。