【譯】【Java】【多線程】飢餓與公平

飢餓與公平

原文地址: http://tutorials.jenkov.com/j...html

若是一個線程沒有被分配到 CPU 執行時間,該線程就處於「飢餓」狀態。若是老是分配不到 CPU 執行時間(由於老是被分配到其餘線程去了),那麼該線程可能會被「餓死」。有一種策略用於避免出現該問題,稱做「公平策略」,即保證全部的線程都能公平地獲得被執行的機會。java

產生飢餓的緣由

在 Java 中,有三種最廣泛的情形會致使飢餓的發生:安全

  1. 高優先級的線程老是吞佔 CPU 執行時間,致使低優先級的線程沒有機會;
  2. 某些線程老是能被容許進入 synchronized 塊,以至某些線程老是得不到機會;
  3. 某些線程在等待指定的對象(即調用了該對象的 wait() 方法)時,徹底得不到喚醒的機會,由於被喚醒的老是別的線程。

高優先級的線程老是吞佔 CPU 執行時間

每一個線程均可以單獨設置優先級。優先級越高,該線程就能得到更多的 CPU 執行時間。優先級的值最低爲 1 最高爲 10。至於如何根據優先級來分配 CPU 執行時間,則依賴於操做系統的具體實現。在大多數應用中,咱們最好不要去擅自修改它。this

線程無限等待進入 synchronized 塊的機會

Java 當中的 synchronized 代碼塊也是致使飢餓的一個因素。它不保證線程進入的順序,因此理論上某個線程可能永遠沒法進入 synchronized 塊,這種狀況下能夠說這個線程就被「餓死」了。操作系統

線程無限等待被鎖對象喚醒的機會

當多個線程同時調用的某個對象的 wait() 方法並等待時,notify() 方法不保證必定能喚醒哪一個指定的線程。因此若是它老是不去喚醒某個線程的話,這個線程就處於永久性地等待當中了。線程

如何在 Java 中實現公平策略

固然咱們沒辦法實現 100% 的絕對公平,但仍是能夠經過一些結構上的設計來增長線程之間的公平性。設計

首先咱們來看一個簡單的 synchronized 代碼塊:code

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

當多個線程調用 doSynchronized() 方法時,只有一個線程可以進入該方法並執行,並且該線程退出該方法後,正在等待的線程中沒法保證哪個纔是接下來能夠進入的。htm

用鎖對象來代替 synchronized 塊

爲了加強公平性,第一步咱們先把 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 對象從隊列中去掉。

關於執行效率的說明

咱們把 LockFairLock 對比一下就會看到後者的 lock() unlock() 增長了不少代碼,它們會致使其執行效率比前者略有降低。這個影響的程度如何,取決於 lock()unlock() 之間的同步代碼的執行時間,該時間越長,則影響就越小。固然同時也取決於鎖自己的使用頻繁程度。

相關文章
相關標籤/搜索