1. 程式人生 > >【Java併發基礎】管程簡介

【Java併發基礎】管程簡介

前言

在Java 1.5之前,Java語言提供的唯一併發語言就是管程,Java 1.5之後提供的SDK併發包也是以管程為基礎的。除了Java之外,C/C++、C#等高階語言也都是支援管程的。

那麼什麼是管程呢?
見名知意,是指管理共享變數以及對共享變數操作的過程,讓它們支援併發。翻譯成Java領域的語言,就是管理類的狀態變數,讓這個類是執行緒安全的。

synchronized關鍵字和wait()、notify()、notifyAll()這三個方法是Java中實現管程技術的組成部分。記得學習作業系統時,線上程一塊還有訊號量機制,管程在功能上和訊號量及PV操作類似,屬於一種程序同步互斥工具。Java選擇管程來實現併發主要還是因為實現管程比較容易。

管程對應的英文是Monitor,直譯為“監視器”,而作業系統領域一般翻譯為“管程”。

在管程的發展史上,先後出現過三種不同的管程模型,分別是Hasen模型、Hoare模型和MESA模型。現在正在廣泛使用的是MESA模型。下面我們便介紹MESA模型。

MESA模型

管程中引入了條件變數的概念,而且每個條件變數都對應有一個等待佇列。條件變數和等待佇列的作用是解決執行緒之間的同步問題。

我們來看一個例子來理解這個模型。多個執行緒對一個共享佇列進行操作。

假設執行緒T1要執行出隊操作,但是這個操作要執行成功的前提是佇列不能為空。這個佇列不能為空就是管程裡的條件變數。若是執行緒T1進入管程後發現佇列是空的,那它就需要在“佇列不空”這個條件變數的等待佇列中等待。

通過呼叫wait()實現。若是用物件A代表“佇列不空”這個條件,那麼執行緒T1需要呼叫A.wait(),來將自己阻塞。
線上程T1進入條件變數的等待佇列後,是允許其他執行緒進入管程的。

再假設之後另外一個執行緒T2執行了入隊操作,入隊操作成功之後,“佇列不空”這個條件對於執行緒T1來說已經滿足了,此時執行緒T2要通知執行緒T1,告訴它呼叫需要的條件已經滿足了。
那麼執行緒T2怎麼通知執行緒T1?執行緒T2呼叫A.notify()來通知A等待佇列中的一個執行緒,此時這個執行緒裡面只有T1,所以notify喚醒的就是執行緒T1,如果當這個條件變數的等待佇列不止T1一個執行緒,我們就需要使用notifyAll()。

當執行緒T1得到通知後,會從等待佇列中出來,重新進入到入口等待佇列中。

使用程式碼說明就如下:(程式碼來自參考[1])
注意,await()和前面的wait()的語義是一樣的;signal()和前面的notify()語義是一樣的(沒有提到的signalAll()notifyAll()語義也是一樣的)。

public class BlockedQueue<T>{
    final Lock lock = new ReentrantLock();
    // 條件變數:佇列不滿  
    final Condition notFull = lock.newCondition();
    // 條件變數:佇列不空  
    final Condition notEmpty = lock.newCondition();

    // 入隊
    void enq(T x) {
        lock.lock();
        try {
            while (佇列已滿){
                // 等待佇列不滿
                notFull.await();
            }  
            // 省略入隊操作...
            // 入隊後, 通知可出隊
            notEmpty.signal();
        }finally {
            lock.unlock();
        }
    }
    // 出隊
    void deq(){
        lock.lock();
        try {
            while (佇列已空){
                // 等待佇列不空
                notEmpty.await();
            }
            // 省略出隊操作...
            // 出隊後,通知可入隊
            notFull.signal();
        }finally {
            lock.unlock();
        }  
    }
}

wait()的正確使用姿勢

對於MESA管程來說,有一個程式設計正規化:

while(條件不滿足) {
  wait();
}

我們在前面介紹等待-通知機制時就提到過這種正規化。這個正規化可以解決“條件曾將滿足過”這個問題。喚醒的時間和獲取到鎖繼續執行的時間是不一致的,被喚醒的執行緒再次執行時可能條件又不滿足了,所以迴圈檢驗條件。

MESA模型的wait()方法還有一個超時引數,為了避免執行緒進入等待佇列永久阻塞。

notify()和notifyAll()分別何時使用

滿足以下三個條件時,可以使用notify(),其餘情況儘量使用notifyAll():

  1. 所有等待執行緒擁有相同的等待條件;
  2. 所有等待執行緒被喚醒後,執行相同的操作;
  3. 只需要喚醒一個執行緒。

三種管程模型在通知執行緒上的區別

Hasen模型、Hoare模型和MESA模型的一個核心區別是當條件滿足後,如何通知相關執行緒。

管程要求同一時刻只允許一個執行緒執行,那當執行緒T2的操作使得執行緒T1等待的條件滿足時,T1和T2究竟誰可以執行呢?

  1. 在Hasen模型裡,要求notify()放在程式碼的最後,這樣T2通知完T1後,T2就結束了,然後T1再執行這樣就可以保證同一時刻只有一個執行緒執行。
  2. 在Hoare模型裡面,T2通知完T1後,T2阻塞,T1馬上執行;等T1執行完,再喚醒t2。比起Hasen模型,T2多了一次阻塞喚醒操作。
  3. 在MESA管程裡,T2通知完T1後,T2還是會接著執行,T1並不立即執行,僅僅是從條件變數的等待佇列進入到入口等待佇列中(但是T1再次執行時,可能條件又不滿足了,所以需要迴圈防方式檢驗條件變數)。這樣的好處是:notify()程式碼不用放到程式碼的最後,T2也沒有多餘的阻塞喚醒操作。

Java語言的內建管程synchronized

Java 參考了 MESA 模型,語言內建的管程(synchronized)對 MESA 模型進行了精簡。MESA 模型中,條件變數可以有多個,Java 語言內建的管程裡只有一個條件變數。模型如下圖所示。(圖來自參考[1])

Java 內建的管程方案(synchronized)使用簡單,synchronized 關鍵字修飾的程式碼塊,在編譯期會自動生成相關加鎖和解鎖的程式碼,但是僅支援一個條件變數;而 Java SDK 併發包實現的管程支援多個條件變數,不過併發包裡的鎖,需要我們自己進行加鎖和解鎖操作。

小結

開始本來打算不寫這篇學習筆記的,但是思考了一下,Java併發實現本就是源於作業系統中的管程,既然要好好介紹Java併發那麼它的來源也應該要好好介紹一下。在學習一個知識的時候,其背後的理論也要好好掌握。

參考:
[1]極客時間專欄王寶令《Java併發程式設計實戰