Java并發(fā)之“饑餓”和“公平鎖”(Starvation and Fairness)

饑餓發(fā)生的原因:

  • 高優(yōu)先級的線程占用了大部分的cpu時間漓糙,低優(yōu)先級線程發(fā)生饑餓
  • 線程被永久堵塞在一個等待進入同步塊的狀態(tài)
  • 線程在等待一個本身(在其上調用wait())也處于永久等待完成的對象

java中實現(xiàn)公平鎖

  • 使用鎖而不是同步塊
  • 公平鎖

如果一個線程的cpu執(zhí)行時間都被其他線程搶占了峡懈,導致得不到cpu執(zhí)行膏燕,這種情況就叫做“饑餓”填大,這個線程就會出現(xiàn)饑餓致死的現(xiàn)象,因為永遠無法得到cpu的執(zhí)行。解決饑餓現(xiàn)象的方法就是實現(xiàn)公平誓篱,保證所有線程都公平的獲得執(zhí)行的機會嗦明。

java中發(fā)生線程饑餓的原因

  • 高優(yōu)先級的線程占用了大部分的cpu時間笼沥,低優(yōu)先級線程發(fā)生饑餓
  • 線程被永久堵塞在一個等待進入同步塊的狀態(tài)
  • 線程在等待一個本身(在其上調用wait())也處于永久等待完成的對象

高優(yōu)先級的線程占用了大部分的cpu時間,低優(yōu)先級線程發(fā)生饑餓

你可以給每個線程單獨的設置優(yōu)先級娶牌。優(yōu)先級越高奔浅,就會獲得越高的cpu執(zhí)行的機會。

線程被永久堵塞在一個等待進入同步塊的狀態(tài)

java 的synchronize語句塊不保證線程進入語句塊的順序诗良,所以這就存在一個可能的問題汹桦,有一個線程一直阻塞在synchronize語句塊,永遠都無法進入synchronize鉴裹。

線程在等待一個本身(在其上調用wait())也處于永久等待完成的對象

同樣的舞骆,類似synchronize,notify也不保證線程被喚醒的順序径荔。所以也存在一個風險督禽,就是一個wait的線程一直處于wait的狀態(tài),永遠也沒有被notify所喚醒猖凛。

java中實現(xiàn)公平鎖

雖然無法實現(xiàn)完全100%公平赂蠢,但是我們仍然可以盡可能的提高線程的公平性。
首先辨泳,我們研究一個簡單的synchronize語句塊:

public class Synchronizer{

  public synchronized void doSynchronized(){
    //do a lot of work which takes a long time
  }

}

如果超過一個線程調用這個方法虱岂,那么只有一個線程可以進入這個方法執(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()調用里面燎窘。要記住的是,當線程正在等待進入lock() 時蹄咖,可以調用wait()釋放其鎖實例對應的同步鎖褐健,使得其他多個線程可以進入lock()方法,并調用wait()方法澜汤。

這回看下doSynchronized()蚜迅,你會注意到在lock()和unlock()之間的注釋:在這兩個調用之間的代碼將運行很長一段時間。進一步設想俊抵,這段代碼將長時間運行谁不,和進入lock()并調用wait()來比較的話。這意味著大部分時間用在等待進入鎖和進入臨界區(qū)的過程是用在wait()的等待中徽诲,而不是被阻塞在試圖進入lock()方法中刹帕。

在早些時候提到過,同步塊不會對等待進入的多個線程誰能獲得訪問做任何保障馏段,同樣當調用notify()時轩拨,wait()也不會做保障一定能喚醒線程因此這個版本的Lock類和doSynchronized()那個版本就保障公平性而言,沒有任何區(qū)別院喜。

但我們能改變這種情況亡蓉。當前的Lock類版本調用自己的wait()方法,** 如果每個線程在不同的對象上調用wait()喷舀,那么只有一個線程會在該對象上調用wait()砍濒,Lock類可以決定哪個對象能對其調用notify(),因此能做到有效的選擇喚醒哪個線程硫麻。**
實際上這就是公平鎖的實現(xiàn)思想

公平鎖

下面來講述將上面Lock類轉變?yōu)楣芥iFairLock爸邢。你會注意到新的實現(xiàn)和之前的Lock類中的同步和wait()/notify()稍有不同。

準確地說如何從之前的Lock類做到公平鎖的設計是一個漸進設計的過程拿愧,每一步都是在解決上一步的問題而前進的:Nested Monitor Lockout, Slipped Conditions和Missed Signals杠河。這些本身的討論雖已超出本文的范圍,但其中每一步的內容都將會專題進行討論。重要的是券敌,每一個調用lock()的線程都會進入一個隊列唾戚,當解鎖后,只有隊列里的第一個線程被允許鎖住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的實例测蹲,并對每個調用lock()的線程進行入隊列莹捡。調用unlock()的線程將從隊列頭部獲取QueueObject,并對其調用doNotify()扣甲,以喚醒在該對象上等待的線程道盏。通過這種方式,在同一時間僅有一個等待線程獲得喚醒文捶,而不是所有的等待線程。這也是實現(xiàn)FairLock公平性的核心所在媒咳。

請注意粹排,在同一個同步塊中,鎖狀態(tài)依然被檢查和設置涩澡,以避免出現(xiàn)滑漏條件启妹。

還需注意到鸥昏,QueueObject實際是一個semaphore。doWait()和doNotify()方法在QueueObject中保存著信號。這樣做以避免一個線程在調用queueObject.doWait()之前被另一個調用unlock()并隨之調用queueObject.doNotify()的線程重入测柠,從而導致信號丟失。queueObject.doWait()調用放置在synchronized(this)塊之外漆撞,以避免被monitor嵌套鎖死祟剔,所以另外的線程可以解鎖,只要當沒有線程在lock方法的synchronized(this)塊中執(zhí)行即可芒涡。

最后柴灯,注意到queueObject.doWait()在try – catch塊中是怎樣調用的。在InterruptedException拋出的情況下费尽,線程得以離開lock()赠群,并需讓它從隊列中移除。

性能考慮

如果比較Lock和FairLock類旱幼,你會注意到在FairLock類中l(wèi)ock()和unlock()還有更多需要深入的地方查描。這些額外的代碼會導致FairLock的同步機制實現(xiàn)比Lock要稍微慢些。究竟存在多少影響,還依賴于應用在FairLock臨界區(qū)執(zhí)行的時長冬三。執(zhí)行時長越大匀油,F(xiàn)airLock帶來的負擔影響就越小,當然這也和代碼執(zhí)行的頻繁度相關长豁。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末钧唐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子匠襟,更是在濱河造成了極大的恐慌钝侠,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酸舍,死亡現(xiàn)場離奇詭異帅韧,居然都是意外死亡,警方通過查閱死者的電腦和手機啃勉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門忽舟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人淮阐,你說我怎么就攤上這事叮阅。” “怎么了泣特?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵浩姥,是天一觀的道長。 經(jīng)常有香客問我状您,道長勒叠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任膏孟,我火速辦了婚禮眯分,結果婚禮上,老公的妹妹穿的比我還像新娘柒桑。我一直安慰自己弊决,他們只是感情好,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布幕垦。 她就那樣靜靜地躺著丢氢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪先改。 梳的紋絲不亂的頭發(fā)上疚察,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音仇奶,去河邊找鬼貌嫡。 笑死比驻,一個胖子當著我的面吹牛,可吹牛的內容都是我干的岛抄。 我是一名探鬼主播别惦,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼夫椭!你這毒婦竟也來了掸掸?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蹭秋,失蹤者是張志新(化名)和其女友劉穎扰付,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仁讨,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡羽莺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了洞豁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盐固。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖丈挟,靈堂內的尸體忽然破棺而出刁卜,到底是詐尸還是另有隱情,我是刑警寧澤曙咽,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布长酗,位于F島的核電站,受9級特大地震影響桐绒,放射性物質發(fā)生泄漏。R本人自食惡果不足惜之拨,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一茉继、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚀乔,春花似錦烁竭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至睬魂,卻和暖如春终吼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背氯哮。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工际跪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓姆打,卻偏偏與公主長得像良姆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子幔戏,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容