併發程式設計基礎 - 管程模型和synchronized原子性

it_lihongmin發表於2020-10-20

    通過上一篇我們知道通過記憶體屏障,在反編譯的組合語言中我們看到基於lock cmpxchg和lock addl 前置指令解決了synchronized和volatile的可見性和有序性問題。也在反彙編中看到了synchronized中的monitorenter和monitorexit,這是synchronized實現原子性的關鍵。monitor可以叫監視器,也可以翻譯為管程,並不是java特有的產物。併發過程中除了有執行緒原子性的問題,我們還需要解決執行緒間的互斥還得資料同步

    在解決併發問題的歷史過程中,最早是使用訊號量處理,在juc包中Semaphore還有其實現支援,將在編髮程式設計工具中分析。往後才是管程模型的提出,但是管程和訊號量是等價的,即管程能實現的功能(或效果)訊號量也能實現。在java中synchronized就是管程模型的實現,只是有c語言實現不方便檢視,但是在JDK5之後juc(java.util.concurrent)包下面的API就是基於volatile(解決了可見性和有序性)+ CAS 模擬管程實現。實現的核心由AQS(AbstractQueuedSynchronizer)實現,CAS使用sun.misc.UnSafe的CAS相關API實現。所以當前先使用ReentrantLock和Condition模擬管程實現【先大概看看管程是什麼東西】,實現執行緒安全的原子性,即操作的過程對外不可見,外面不能看見執行中的中間狀態,那麼管程的思想就是執行的過程封裝起來,滿足條件後再喚醒其他(執行緒)操作,也就解決了原子性。

/**
 *  使用{@link ReentrantLock} 和 {@link Condition} 模擬管程模型;在synchronized中只能有一個條件佇列,
 *  而當前可以建立多個Condition即可
 *
 *  <p>
 *      對於入隊操作,如果佇列已滿,就需要等待直到佇列不滿,所以這裡用了notFull.await();。
 *      對於出隊操作,如果佇列為空,就需要等待直到佇列不空,所以就用了notEmpty.await();。
 *      如果入隊成功,那麼佇列就不空了,就需要通知條件變數:佇列不空notEmpty對應的等待佇列。
 *      如果出隊成功,那就佇列就不滿了,就需要通知條件變數:佇列不滿notFull對應的等待佇列。
 *
 *  <p>
 *      我們知道 {@link Object#wait()}、{@link Object#notify()}、{@link Object#notifyAll()} 只能在 synchronized中使用
 *      同樣 {@link Condition#await()}、{@link Condition#signal()}、{@link Condition#signalAll()} 只能在Lock&Condition中使用
 *      並且這三個方法一一對應, 需要注意上面三個是{@link Object}的方法,所以在下面的{@link Condition}中同樣存在,千萬別呼叫錯了
 *
 * @author kevin
 * @date 2020/7/29 23:51
 * @since 1.0.0
 *
 * @see LinkedBlockingQueue#takeLock
 * @see LinkedBlockingQueue#putLock
 *
 * @see LinkedBlockingQueue#notFull
 * @see LinkedBlockingQueue#notEmpty
 */
public class BlockedQueue {

    /** 建立一個公平鎖 */
    final Lock lock = new ReentrantLock(true);

    /** 條件變數:佇列不滿 */
    final Condition notFull = lock.newCondition();

    /** 條件變數:佇列不空 */
    final Condition notEmpty = lock.newCondition();

    /**
     *  入隊操作
     */
    void enqueue() throws InterruptedException {
        lock.lock();
        try {
            while (佇列已滿) {
                // 等待佇列不滿
                notFull.await();
            }
            // 省略入隊操作

            // 入隊後通知可出隊
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    void dequeue() throws InterruptedException {
        lock.lock();
        try {
            while (佇列已空) {
                // 等待佇列不空
                notEmpty.await();
            }
            // 省略出佇列操作,省略一萬行
            // 出佇列後,通知可入隊
            notFull.signal();
        } finally {
            lock.unlock();
        }
    }
}

管程模型在管程模型分為三種(Java選擇了MESA管程模型):

1)、Hasen模型

    Hasen模型要求notify放到最後,這樣T2執行緒通知T1後,T2執行緒就結束了,然後T1執行完,這樣就能保證同一時刻只有一個執行緒在執行。

2)、Hoare模型

    Hoare模型裡面,T2執行緒通知完T1執行緒後,T2馬上阻塞,T1馬上執行;等T1執行完之後再喚醒T2執行緒,也能保證同一時刻只有一個執行緒在執行,但是T2多了一次阻塞喚醒操作。

3)、MESA管程模型(Java使用MESA模型實現)

    MESA模型中,T2喚醒T1之後,T2還是會接著執行,T1並不立即執行,僅僅是從條件變數佇列等待佇列中。

    好處:notify(或notifyAll)、signal(或signalAll)不用放到程式碼的最後,T2也沒有多餘的阻塞喚醒操作

    壞處:T1執行的時候,可能曾經滿足過條件,現在已經不能滿足了,需要增加迴圈驗證條件方式(個人理解也算是樂觀鎖的思想)。

MESA管程模型:

看到上圖的Java MESA管程模型,就理解synchronized與看似從天而降的的Object的 wait、notify、notifyAll方法,synchronized可以修飾

1、方法塊前提是括號中需要有物件即Object

2、修飾普通方法,修飾的是 Object物件

3、修飾靜態方法,修飾的是 .class,可以理解為也是Object物件

所以synchronized關鍵字,處理的是Object物件(其子類)的物件頭的狀態,後續再詳細分析。上中的佇列模型即反彙編之後看到的monitorenter、monitorexit,底層封裝了條件滿足後,對佇列中的 wait(等待)、notify(notifyAll 喚醒)操作。結合c底層的對應的佇列,和管程物件,可以理解為下圖:

 

注意:

除非經過深思熟慮(或者有非常的把握)儘量使用Object的 notifyAll(不要使用notify)、Condition的 signalAll(不要使用signal),除非滿足以下三個條件【否則可能照成某些執行緒的飢餓等】:

1)、所以等待執行緒擁有相同的等待條件

2)、所以等待執行緒被喚醒後,執行相同的操作

3)、只需要喚醒一個執行緒

 

 

 

 

 

 

 

 

 

相關文章