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