饑餓發(fā)生的原因:
- 高優(yōu)先級的線程占用了大部分的cpu時間,低優(yōu)先級線程發(fā)生饑餓
- 線程被永久堵塞在一個等待進入同步塊的狀態(tài)
- 線程在等待一個本身(在其上調(diào)用wait())也處于永久等待完成的對象
java中實現(xiàn)公平鎖
- 使用鎖而不是同步塊
- 公平鎖
如果一個線程的cpu執(zhí)行時間都被其他線程搶占了,導(dǎo)致得不到cpu執(zhí)行,這種情況就叫做“饑餓”,這個線程就會出現(xiàn)饑餓致死的現(xiàn)象,因為永遠無法得到cpu的執(zhí)行。解決饑餓現(xiàn)象的方法就是實現(xiàn)公平,保證所有線程都公平的獲得執(zhí)行的機會。
java中發(fā)生線程饑餓的原因
- 高優(yōu)先級的線程占用了大部分的cpu時間,低優(yōu)先級線程發(fā)生饑餓
- 線程被永久堵塞在一個等待進入同步塊的狀態(tài)
- 線程在等待一個本身(在其上調(diào)用wait())也處于永久等待完成的對象
高優(yōu)先級的線程占用了大部分的cpu時間,低優(yōu)先級線程發(fā)生饑餓
你可以給每個線程單獨的設(shè)置優(yōu)先級。優(yōu)先級越高,就會獲得越高的cpu執(zhí)行的機會。
線程被永久堵塞在一個等待進入同步塊的狀態(tài)
java 的synchronize語句塊不保證線程進入語句塊的順序,所以這就存在一個可能的問題,有一個線程一直阻塞在synchronize語句塊,永遠都無法進入synchronize。
線程在等待一個本身(在其上調(diào)用wait())也處于永久等待完成的對象
同樣的,類似synchronize,notify也不保證線程被喚醒的順序。所以也存在一個風(fēng)險,就是一個wait的線程一直處于wait的狀態(tài),永遠也沒有被notify所喚醒。
java中實現(xiàn)公平鎖
雖然無法實現(xiàn)完全100%公平,但是我們?nèi)匀豢梢员M可能的提高線程的公平性。
首先,我們研究一個簡單的synchronize語句塊:
public class Synchronizer{
public synchronized void doSynchronized(){
//do a lot of work which takes a long time
}
}
如果超過一個線程調(diào)用這個方法,那么只有一個線程可以進入這個方法執(zhí)行,其他線程都必須等待,直到該線程退出。而且我們無法知道下一個進入synchronize的語句塊的線程會是那一個
使用lock而不是synchronize
為了增加等待線程的公平性,我們可以用lock來替換synchronize
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();
}
}
lock類的一個簡單的實現(xiàn)如下:
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的實現(xiàn),如果存在多線程并發(fā)訪問lock(),這些線程將阻塞在對lock()方法的訪問上。另外,如果鎖已經(jīng)鎖上(校對注:這里指的是isLocked等于true時),這些線程將阻塞在while(isLocked)循環(huán)的wait()調(diào)用里面。要記住的是,當(dāng)線程正在等待進入lock() 時,可以調(diào)用wait()釋放其鎖實例對應(yīng)的同步鎖,使得其他多個線程可以進入lock()方法,并調(diào)用wait()方法。
這回看下doSynchronized(),你會注意到在lock()和unlock()之間的注釋:在這兩個調(diào)用之間的代碼將運行很長一段時間。進一步設(shè)想,這段代碼將長時間運行,和進入lock()并調(diào)用wait()來比較的話。這意味著大部分時間用在等待進入鎖和進入臨界區(qū)的過程是用在wait()的等待中,而不是被阻塞在試圖進入lock()方法中。
在早些時候提到過,同步塊不會對等待進入的多個線程誰能獲得訪問做任何保障,同樣當(dāng)調(diào)用notify()時,wait()也不會做保障一定能喚醒線程因此這個版本的Lock類和doSynchronized()那個版本就保障公平性而言,沒有任何區(qū)別。
但我們能改變這種情況。當(dāng)前的Lock類版本調(diào)用自己的wait()方法,** 如果每個線程在不同的對象上調(diào)用wait(),那么只有一個線程會在該對象上調(diào)用wait(),Lock類可以決定哪個對象能對其調(diào)用notify(),因此能做到有效的選擇喚醒哪個線程。**
實際上這就是公平鎖的實現(xiàn)思想
公平鎖
下面來講述將上面Lock類轉(zhuǎn)變?yōu)楣芥iFairLock。你會注意到新的實現(xiàn)和之前的Lock類中的同步和wait()/notify()稍有不同。
準(zhǔn)確地說如何從之前的Lock類做到公平鎖的設(shè)計是一個漸進設(shè)計的過程,每一步都是在解決上一步的問題而前進的:Nested Monitor Lockout, Slipped Conditions和Missed Signals。這些本身的討論雖已超出本文的范圍,但其中每一步的內(nèi)容都將會專題進行討論。重要的是,每一個調(diào)用lock()的線程都會進入一個隊列,當(dāng)解鎖后,只有隊列里的第一個線程被允許鎖住Farlock實例,所有其它的線程都將處于等待狀態(tài),直到他們處于隊列頭部。
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新創(chuàng)建了一個QueueObject的實例,并對每個調(diào)用lock()的線程進行入隊列。調(diào)用unlock()的線程將從隊列頭部獲取QueueObject,并對其調(diào)用doNotify(),以喚醒在該對象上等待的線程。通過這種方式,在同一時間僅有一個等待線程獲得喚醒,而不是所有的等待線程。這也是實現(xiàn)FairLock公平性的核心所在。
請注意,在同一個同步塊中,鎖狀態(tài)依然被檢查和設(shè)置,以避免出現(xiàn)滑漏條件。
還需注意到,QueueObject實際是一個semaphore。doWait()和doNotify()方法在QueueObject中保存著信號。這樣做以避免一個線程在調(diào)用queueObject.doWait()之前被另一個調(diào)用unlock()并隨之調(diào)用queueObject.doNotify()的線程重入,從而導(dǎo)致信號丟失。queueObject.doWait()調(diào)用放置在synchronized(this)塊之外,以避免被monitor嵌套鎖死,所以另外的線程可以解鎖,只要當(dāng)沒有線程在lock方法的synchronized(this)塊中執(zhí)行即可。
最后,注意到queueObject.doWait()在try – catch塊中是怎樣調(diào)用的。在InterruptedException拋出的情況下,線程得以離開lock(),并需讓它從隊列中移除。
性能考慮
如果比較Lock和FairLock類,你會注意到在FairLock類中l(wèi)ock()和unlock()還有更多需要深入的地方。這些額外的代碼會導(dǎo)致FairLock的同步機制實現(xiàn)比Lock要稍微慢些。究竟存在多少影響,還依賴于應(yīng)用在FairLock臨界區(qū)執(zhí)行的時長。執(zhí)行時長越大,F(xiàn)airLock帶來的負(fù)擔(dān)影響就越小,當(dāng)然這也和代碼執(zhí)行的頻繁度相關(guān)。