鍍金池/ 教程/ Java/ 饑餓和公平
Slipped Conditions
阻塞隊列
無阻塞算法
嵌套管程鎖死
Java 并發(fā)性和多線程介紹
死鎖
線程安全及不可變性
并發(fā)編程模型
Java 中的讀/寫鎖
剖析同步器
競態(tài)條件與臨界區(qū)
多線程的優(yōu)點
CAS
線程通信
如何創(chuàng)建并運行 java 線程
阿姆達爾定律
避免死鎖
信號量
多線程的代價
饑餓和公平
線程池
重入鎖死
Java 中的鎖
Java 內(nèi)存模型
線程安全與共享資源
Java 同步塊

饑餓和公平

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

下面是本文討論的主題:

  1. Java 中導(dǎo)致饑餓的原因:

    • 高優(yōu)先級線程吞噬所有的低優(yōu)先級線程的 CPU 時間。
    • 線程被永久堵塞在一個等待進入同步塊的狀態(tài)。
    • 線程在等待一個本身也處于永久等待完成的對象(比如調(diào)用這個對象的 wait 方法)。
  2. 在 Java 中實現(xiàn)公平性方案,需要:

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

Java 中導(dǎo)致饑餓的原因

在 Java 中,下面三個常見的原因會導(dǎo)致線程饑餓:

  1. 高優(yōu)先級線程吞噬所有的低優(yōu)先級線程的 CPU 時間。
  2. 線程被永久堵塞在一個等待進入同步塊的狀態(tài),因為其他線程總是能在它之前持續(xù)地對該同步塊進行訪問。
  3. 線程在等待一個本身(在其上調(diào)用 wait())也處于永久等待完成的對象,因為其他線程總是被持續(xù)地獲得喚醒。

高優(yōu)先級線程吞噬所有的低優(yōu)先級線程的 CPU 時間

你能為每個線程設(shè)置獨自的線程優(yōu)先級,優(yōu)先級越高的線程獲得的 CPU 時間越多,線程優(yōu)先級值設(shè)置在 1 到 10 之間,而這些優(yōu)先級值所表示行為的準(zhǔn)確解釋則依賴于你的應(yīng)用運行平臺。對大多數(shù)應(yīng)用來說,你最好是不要改變其優(yōu)先級值。

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

Java 的同步代碼區(qū)也是一個導(dǎo)致饑餓的因素。Java 的同步代碼區(qū)對哪個線程允許進入的次序沒有任何保障。這就意味著理論上存在一個試圖進入該同步區(qū)的線程處于被永久堵塞的風(fēng)險,因為其他線程總是能持續(xù)地先于它獲得訪問,這即是“饑餓”問題,而一個線程被“饑餓致死”正是因為它得不到 CPU 運行時間的機會。

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

如果多個線程處在 wait()方法執(zhí)行上,而對其調(diào)用 notify()不會保證哪一個線程會獲得喚醒,任何線程都有可能處于繼續(xù)等待的狀態(tài)。因此存在這樣一個風(fēng)險:一個等待線程從來得不到喚醒,因為其他等待線程總是能被獲得喚醒。

在 Java 中實現(xiàn)公平性

雖 Java 不可能實現(xiàn) 100% 的公平性,我們依然可以通過同步結(jié)構(gòu)在線程間實現(xiàn)公平性的提高。

首先來學(xué)習(xí)一段簡單的同步態(tài)代碼:

public class Synchronizer{

    public synchronized void doSynchronized(){

    //do a lot of work which takes a long time

    }
}

如果有一個以上的線程調(diào)用 doSynchronized()方法,在第一個獲得訪問的線程未完成前,其他線程將一直處于阻塞狀態(tài),而且在這種多線程被阻塞的場景下,接下來將是哪個線程獲得訪問是沒有保障的。

使用鎖方式替代同步塊

為了提高等待線程的公平性,我們使用鎖方式來替代同步塊。

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 類做的一個實現(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(),因此能做到有效的選擇喚醒哪個線程。

公平鎖

下面來講述將上面 Lock 類轉(zhuǎn)變?yōu)楣芥i FairLock。你會注意到新的實現(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 類中 lock()和 unlock()還有更多需要深入的地方。這些額外的代碼會導(dǎo)致 FairLock 的同步機制實現(xiàn)比 Lock 要稍微慢些。究竟存在多少影響,還依賴于應(yīng)用在 FairLock 臨界區(qū)執(zhí)行的時長。執(zhí)行時長越大,F(xiàn)airLock 帶來的負擔(dān)影響就越小,當(dāng)然這也和代碼執(zhí)行的頻繁度相關(guān)。

上一篇:Java 內(nèi)存模型下一篇:重入鎖死