1. 程式人生 > >18、Java並發性和多線程-饑餓與公平

18、Java並發性和多線程-饑餓與公平

cnblogs 同步鎖 丟失 允許 color dex 訪問 嵌套 時間

以下內容轉自http://ifeve.com/starvation-and-fairness/:

如果一個線程因為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帶來的負擔影響就越小,當然這也和代碼執行的頻繁度相關。

18、Java並發性和多線程-饑餓與公平