摘要:語言在之前,提供的唯一的并發原語就是管程,而且之后提供的并發包,也是以管程技術為基礎的。但是管程更容易使用,所以選擇了管程。線程進入條件變量的等待隊列后,是允許其他線程進入管程的。并發編程里兩大核心問題互斥和同步,都可以由管程來幫你解決。
并發編程這個技術領域已經發展了半個世紀了。有沒有一種核心技術可以很方便地解決我們的并發問題呢?這個問題, 我會選擇 Monitor(管程)技術。
Java 語言在 1.5 之前,提供的唯一的并發原語就是管程,而且 1.5 之后提供的 SDK 并發包,也是以管程技術為基礎的。除此之外,C/C++、C# 等高級語言也都支持管程。
操作系統原理課程告訴我們,用信號量能解決所有并發問題。但是為什么 Java 在 1.5 之前僅僅提供了 synchronized 關鍵字及 wait()、notify()、notifyAll() 這三個看似從天而降的方法?當然這里因為 Java 采用的是管程技術,synchronized 關鍵字及 wait()、notify()、notifyAll() 這三個方法都是管程的組成部分。并且
管程和信號量是等價的,所謂等價指的是用管程能夠實現信號量,也能用信號量實現管程。
但是管程更容易使用,所以 Java 選擇了管程。
管程,對應的英文是 Monitor,很多 Java 領域的同學都喜歡將其翻譯成“監視器”,這是直譯。操作系統領域一般都翻譯成“管程”,這個是意譯,在這里我更傾向于使用“管程”。
管程,指的是管理共享變量以及對共享變量的操作過程,讓他們支持并發。
翻譯為 Java 領域的語言,就是管理類的成員變量和成員方法,讓這個類是線程安全的。那管程是怎么管的呢?
MESA 模型在管程的發展史上,先后出現過三種不同的管程模型,分別是:Hasen 模型、Hoare 模型和 MESA 模型。其中,現在廣泛應用的是 MESA 模型,并且 Java 管程的實現參考的也是 MESA 模型。所以我們重點介紹一下 MESA 模型。
在并發編程領域,有兩大核心問題:
一個是互斥,即同一時刻只允許一個線程訪問共享資源;
另一個是同步,即線程之間如何通信、協作。這兩大問題,管程都是能夠解決的。
我們先來看看管程是如何解決互斥問題的。
管程解決互斥問題的思路很簡單,就是將共享變量及其對共享變量的操作統一封裝起來。在下圖中,管程 X 將共享變量 queue 這個隊列和相關的操作入隊 enq()、出隊 deq() 都封裝起來了;線程 A 和線程 B 如果想訪問共享變量 queue,只能通過調用管程提供的 enq()、deq() 方法來實現;enq()、deq() 保證互斥性,只允許一個線程進入管程。從中可以看出,管程模型和面向對象高度契合的。而我在前面章節介紹的互斥鎖用法,其背后的模型其實就是它。
管程模型的代碼化語義
那管程如何解決線程間的同步問題的。
這個就比較復雜了,我們來看下 MESA 管程模型示意圖,它詳細描述了 MESA 模型的主要組成部分。
在管程模型里,共享變量和對共享變量的操作是被封裝起來的,圖中最外層的框就代表封裝的意思。框的上面只有一個入口,并且在入口旁邊還有一個入口等待隊列。當多個線程同時試圖進入管程內部時,只允許一個線程進入,其他線程則在入口等待隊列中等待。
管程里還引入了條件變量的概念,而且每個條件變量都對應有一個等待隊列,如下圖,條件變量 A 和條件變量 B 分別都有自己的等待隊列。
MESA 管程模型圖
那條件變量和等待隊列的作用是什么呢?其實就是解決線程同步問題。你也可以結合上面提到的入隊出隊例子加深一下理解。
其他關于管程的定義,加深我們的理解
管程是定義了一個數據結構和能為并發所執行的一組操作,這組操作能夠進行同步和改變管程中的數據。這相當于對臨界資源的同步操作都集中進行管理,凡是要訪問臨界資源的進程或線程,都必須先通過管程,由管程的這套機制來實現多進程或線程對同一個臨界資源的互斥訪問和使用。管程的同步主要通過condition類型的變量(條件變量),條件變量可執行操作wait()和signal()。管程一般是由語言編譯器進行封裝,體現出OOP中的封裝思想,也如老師所講的,管程模型和面向對象高度契合的。
假設有個線程 T1 執行出隊操作,不過需要注意的是執行出隊操作,有個前提條件,就是隊列不能是空的,而隊列不空這個前提條件就是管程里的條件變量。 如果線程 T1 進入管程后恰好發現隊列是空的,那怎么辦呢?等待啊,去哪里等呢?就去條件變量對應的等待隊列里面等。此時線程 T1 就去“隊列不空”這個條件變量的等待隊列中等待。線程 T1 進入條件變量的等待隊列后,是允許其他線程進入管程的。
再假設之后另外一個線程 T2 執行入隊操作,入隊操作執行成功之后,“隊列不空”這個條件對于線程 T1 來說已經滿足了,此時線程 T2 要通知 T1,告訴它需要的條件已經滿足了。當線程 T1 得到通知后,會從等待隊列里面出來,但是出來之后不是馬上執行,而是重新進入到入口等待隊列里面。
條件變量及其等待隊列我們講清楚了,下面再說說 wait()、notify()、notifyAll() 這三個操作。前面提到線程 T1 發現“隊列不空”這個條件不滿足,需要進到對應的等待隊列里等待。這個過程就是通過調用 wait() 來實現的。如果我們用對象 A 代表“隊列不空”這個條件,那么線程 T1 需要調用 A.wait()。同理當“隊列不空”這個條件滿足時,線程 T2 需要調用 A.notify() 來通知 A 等待隊列中的一個線程,此時這個隊列里面只有線程 T1。至于 notifyAll() 這個方法,它可以通知等待隊列中的所有線程。
下面的代碼實現的是一個阻塞隊列,阻塞隊列有兩個操作分別是入隊和出隊,這兩個方法都是先獲取互斥鎖,類比管程模型中的入口。
對于入隊操作,如果隊列已滿,就需要等待直到隊列不滿,所以這里用了notFull.await();
對于出隊操作,如果隊列為空,就需要等待直到隊列不空,所以就用了notEmpty.await();
如果入隊成功,那么隊列就不空了,就需要通知條件變量:隊列不空notEmpty對應的等待隊列。
如果出隊成功,那就隊列就不滿了,就需要通知條件變量:隊列不滿notFull對應的等待隊列。
public class BlockedQueue{ 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(); } } }
在這段示例代碼中,我們用了 Java 并發包里面的 Lock 和 Condition,這個例子只是先讓你明白條件變量及其等待隊列是怎么回事。
注意這里只是舉個例子,這里的行為只是跟管程類似,但并不是具體實現,這里拿來舉個例子。wait() 的正確姿勢
但是有一點,需要再次提醒,對于 MESA 管程來說,有一個編程范式,就是需要在一個 while 循環里面調用 wait()。這個是 MESA 管程特有的
while(條件不滿足) { wait(); }
Hasen 模型、Hoare 模型和 MESA 模型的一個核心區別就是當條件滿足后,如何通知相關線程。管程要求同一時刻只允許一個線程執行,那當線程 T2 的操作使線程 T1 等待的條件滿足時,T1 和 T2 究竟誰可以執行呢?
Hasen 模型里面,要求 notify() 放在代碼的最后,這樣 T2 通知完 T1 后,T2 就結束了,然后 T1 再執行,這樣就能保證同一時刻只有一個線程執行。
Hoare 模型里面,T2 通知完 T1 后,T2 阻塞,T1 馬上執行;等 T1 執行完,再喚醒 T2,也能保證同一時刻只有一個線程執行。但是相比 Hasen 模型,T2 多了一次阻塞喚醒操作。
MESA 管程里面,T2 通知完 T1 后,T2 還是會接著執行,T1 并不立即執行,僅僅是從條件變量的等待隊列進到入口等待隊列里面。這樣做的好處是 notify() 不用放到代碼的最后,T2 也沒有多余的阻塞喚醒操作。但是也有個副作用,就是當 T1 再次執行的時候,可能曾經滿足的條件,現在已經不滿足了,所以需要以循環方式檢驗條件變量。
notify() 何時可以使用還有一個需要注意的地方,就是 notify() 和 notifyAll() 的使用,前面章節,我曾經介紹過,
除非經過深思熟慮,否則盡量使用 notifyAll()。那什么時候可以使用 notify() 呢?
需要滿足以下三個條件:
所有等待線程擁有相同的等待條件;
所有等待線程被喚醒后,執行相同的操作;
只需要喚醒一個線程。
比如上面阻塞隊列的例子中,對于“隊列不滿”這個條件變量,其阻塞隊列里的線程都是在等待“隊列不滿”這個條件,反映在代碼里就是下面這 3 行代碼。對所有等待線程來說,都是執行這 3 行代碼,重點是 while 里面的等待條件是完全相同的
while (隊列已滿){ // 等待隊列不滿 notFull.await(); }
所有等待線程被喚醒后執行的操作也是相同的,都是下面這幾行:
// 省略入隊操作... // 入隊后, 通知可出隊 notEmpty.signal();
同時也滿足第 3 條,只需要喚醒一個線程。所以上面阻塞隊列的代碼,使用 signal() 是可以的。
總結Java 參考了 MESA 模型,語言內置的管程(synchronized)對 MESA 模型進行了精簡。MESA 模型中,條件變量可以有多個,Java 語言內置的管程里只有一個條件變量。具體如下圖所示。
Java 內置的管程方案(synchronized)使用簡單,synchronized 關鍵字修飾的代碼塊,在編譯期會自動生成相關加鎖和解鎖的代碼,但是僅支持一個條件變量;而 Java SDK 并發包實現的管程支持多個條件變量,不過并發包里的鎖,需要開發人員自己進行加鎖和解鎖操作。
并發編程里兩大核心問題——互斥和同步,都可以由管程來幫你解決。學好管程,理論上所有的并發問題你都可以解決,并且很多并發工具類底層都是管程實現的,所以學好管程,就是相當于掌握了一把并發編程的萬能鑰匙。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74346.html
摘要:那并發里面的理論和模型是什么呢那便要從操作系統中解決并發問題的一種模型管程講起了。當一個進程使用完管程后,它必須釋放管程并喚醒等待管程的某一個進程。總結在并發編程領域,有兩大核心問題互斥和同步,而這兩個問題,管程模型都可以解決。 為什么需要了解管程 Java并發編程是Java中高級程序員必備的一項技能,但是真正學明白并發編程也并非易事。正如Java并發編程實踐中的一句話編寫正確的程序并...
摘要:請參看前一篇文章并發學習筆記一原子性可見性有序性問題六等待通知機制什么是等待通知機制當線程不滿足某個條件,則進入等待狀態如果線程滿足要求的某個條件后,則通知等待的線程重新執行。經極客時間并發編程實戰專欄內容學習整理 請參看前一篇文章:Java 并發學習筆記(一)——原子性、可見性、有序性問題 六、等待—通知機制 什么是等待通知—機制?當線程不滿足某個條件,則進入等待狀態;如果線程滿足要...
摘要:當前線程使用將對象頭的替換為鎖記錄指針,如果成功,當前線程獲得鎖如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。重量級鎖是悲觀鎖的一種,自旋鎖輕量級鎖與偏向鎖屬于樂觀鎖。 操作系統在面對線程間同步的時候,會支持例如semaphore信號量和mutex互斥量等同步原語,而monitor是在編程語言中被實現的,下面介紹一下java中monitor(監視器/管程:管理共享變量以...
摘要:誕生之處就支持多線程,所以自然有解決這些問題的辦法,而且在編程語言領域處于領先地位。,線程規則這條是關于線程啟動的。在語言里面,的語義本質上是一種可見性,意味著事件對事件來說是可見的,無論事件和事件是否發生在同一個線程里。 之前我們說了:1,可見性2,原子性3,有序性3個并發BUG的之源,這三個也是編程領域的共性問題。Java誕生之處就支持多線程,所以自然有解決這些問題的辦法,而且在編...
摘要:但是有引入了新的問題線程不安全,返回的對象可能還沒有初始化。如果只有一個線程調用是沒有問題的因為不管步驟如何調換,保證返回的對象是已經構造好了。這種特殊情況稱之為指令重排序采用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理。 目錄 雙重檢測鎖的演變過程 利用HappensBefore分析并發問題 無volatile的雙重檢測鎖 雙重檢測鎖的演變過程 synch...
閱讀 1057·2021-11-12 10:34
閱讀 984·2021-09-30 09:56
閱讀 668·2019-08-30 15:54
閱讀 2601·2019-08-30 11:14
閱讀 1464·2019-08-29 16:44
閱讀 3203·2019-08-29 16:35
閱讀 2489·2019-08-29 16:22
閱讀 2440·2019-08-29 15:39