閱讀684 返回首頁    go 阿裏雲 go 技術社區[雲棲]


饑餓和公平

如果一個線程因為CPU時間全部被其他線程搶走而得不到CPU運行時間,這種狀態被稱之為“饑餓”。而該線程被“饑餓致死”正是因為它得不到CPU運行時間的機會。解決饑餓的方案被稱之為“公平性” – 即所有線程均能公平地獲得運行機會。

 下麵是本文討論的主題:

1. Java中導致饑餓的原因:

  • 高優先級線程吞噬所有的低優先級線程的CPU時間。
  • 線程被永久堵塞在一個等待進入同步塊的狀態。
  • 線程在等待一個本身也處於永久等待完成的對象(比如調用這個對象的wait方法)。

2. 在Java中實現公平性方案,需要:

  • 使用鎖,而不是同步塊。
  • 公平鎖。
  • 注意性能方麵。

Java中導致饑餓的原因

在Java中,下麵三個常見的原因會導致線程饑餓:

  1. 高優先級線程吞噬所有的低優先級線程的CPU時間。
  2. 線程被永久堵塞在一個等待進入同步塊的狀態,因為其他線程總是能在它之前持續地對該同步塊進行訪問。
  3. 線程在等待一個本身(在其上調用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

  上一篇:go  Netty框架中的@Skip使用說明
  下一篇:go  非阻塞算法