多執行緒知識梳理(4) synchronized 三部曲之等待 通知模型

澤毛發表於2017-12-21

一、概述

在前面兩篇文章當中,我們介紹了synchronized的基本使用和原理,但是在使用synchronized保證資料一致性的同時,我們希望能夠讓執行緒之間進行一些互動邏輯,也是我們今天要介紹的等待/通知模型,那麼就需要使用到wait/notify

二、等待/通知相關方法

2.1 方法說明

下面,我們先介紹等待/通知機制的相關方法,首先要說明兩點:

  • 這些都是Object定義的方法
  • 呼叫這些方法的前提條件是:該執行緒已經獲得了Object物件所關聯的鎖,也就是說它們需要位於synchronized修飾的同步程式碼塊中。

**(a) wait() ** 呼叫該方法的執行緒進入等待狀態,並釋放它所獲取的物件鎖,只有出現這兩種情況之一,它才會從wait方法中返回,否則將會一直處於等待狀態:

  • 其它執行緒通過notify / notifyAll方法通知該執行緒,並且該執行緒獲取到了物件鎖
  • 執行緒被中斷

(b) wait(long) / wait(long, int)wait方法相同,差別是增加一種從wait方法返回的情況:等待的時間已經到了,並且獲取到了物件鎖。

(c) notify() 通知位於等待佇列中的第一個執行緒,使其從wait()方法返回,而被通知的執行緒的繼續執行需要等到它獲得物件所為止。 需要注意,呼叫notify方法後,並不會立刻釋放它所持有的物件鎖,這需要等到它執行完同步程式碼塊為止。

(d) notifyAll()notify()類似,但是它是通知所有在物件上等待的執行緒。

2.2 實現原理

通過上面的介紹,我們可以看到,在整個等待/通知機制當中,執行緒被掛起時主要有以下三種狀態:等待狀態、超時等待狀態、阻塞狀態,這些狀態都是通過synchroized所修飾的物件來實現的。

在前面我們介紹synchronized原理的時候,曾經說過每個物件都會和一個Monitor相關聯,其實每個Monitor又包含有兩個佇列:等待佇列和同步佇列,其中等待佇列中存放是進入等待狀態的執行緒,而同步佇列中存放的是等待獲取鎖的執行緒。

下面,我們通過一段簡單的虛擬碼來立即兩個執行緒的狀態轉換過程:

synchronized public void waitThread() {
    //執行a方法.
    wait();
    //執行b方法
}

synchronized public void notifyThread() {
    //執行c方法
    notify();
    //執行d方法
}
複製程式碼

我們有AB兩個執行緒,我們模擬以下的一系列行為:

(1) A 執行緒執行 waitThread 方法 此時由於物件鎖沒有被任何執行緒持有,因此,A執行緒成為物件鎖的持有者:

多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型
(2) B 執行緒執行 notifyThread 方法B執行緒執行notifyThread方法時,由於此時物件鎖已經被A執行緒持有,因此它被加入到同步佇列中:
多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型
(3) A 執行緒執行 a 方法
多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型
**(4) A 執行緒執行 wait 方法 ** 當A執行緒執行wait方法後,它會釋放物件鎖,並加入到同步佇列當中,而B執行緒則成為物件鎖新的持有者:
多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型
(5) B 執行緒執行 c 方法
多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型
(6) B 執行緒執行 notify 方法 此時會喚醒等待佇列中A執行緒,但是此時B執行緒仍然持有物件鎖,因此,A執行緒只能被加入到同步佇列:
多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型
(7) B 執行緒執行 d 方法
多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型
(8) B 執行緒從 notifyThread 方法返回 此時A執行緒重新獲取到物件鎖,因此它被從同步佇列中取出,繼續執行接下來的邏輯:
多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型
(9) A 執行緒執行 b 方法
多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型
(10) A 執行緒從 waitThread 方法中返回A執行緒從同步方法返回之後,那麼會釋放它所持有的鎖
多執行緒知識梳理(4)   synchronized 三部曲之等待 通知模型

三、等待/通知的經典正規化

對於等待/通知模型,我們可以總結出它的經典正規化,分別針對等待方和通知方。

3.1 等待方

等待方遵循如下的原則:

  • 獲取物件的鎖
  • 如果條件不滿足,那麼呼叫物件的wait方法,被通知後仍然需要檢查條件
  • 條件滿足則繼續執行對應的邏輯

對應的虛擬碼為:

synchronized( 物件 ) {
    while( 條件不滿足 ) {
        物件.wait();
    }
    對應的處理邏輯
}
複製程式碼

3.2 通知方

通知方遵循如下的原則:

  • 獲得物件的鎖
  • 改變條件
  • 通知所有等待在物件上的執行緒
synchronized( 物件 ) {
    改變條件;
    物件.notifyAll();
}
複製程式碼

相關文章