-
什么是管程?
它是monitor在操作系統領域中的間接翻譯,也可以稱它為監視器。那管程的具體作用是什么呢?:它是描述並實現對共享變量的管理與操作 ,使其在多線程環境下能正確執行的一個管理策略。基於這個定義,我們也可以把管程當作一個臨界資源區的管理策略,管程的實現可以是多樣的。下面我們就來介紹一下前人已經總結出來的實現模型。
-
管程的策略實現模型
在歷史中,管程模型有三種,它們分別是:Hasen 模型、Hoare 模型和 MESA 模型。在介紹模型之前:我們需要先了解清楚相關術語及數據結構。
enterQueue:管程入口隊列,當線程在申請進入管程中發現管程已被占用,那會被放入該隊列並進入阻塞狀態。
varQueue:條件變量等待隊列,當線程執行過程中,條件變量不符合要求,線程被阻塞時,線程會被放入該隊列。
signalQueue:喚醒線程隊列,當管程中的線程主動喚醒在varQueue中的隊列時,它會被放入該隊列。它比enterQueue的線程有優先權。(這是Hasen 模型、Hoare 模型特有的隊列)
condition variables:條件變量,它是存在於管程中的,條件變量通常是由程序賦予意義,程序通過判斷條件變量是否符合所需條件來調用同步與互斥操作。
阻塞操作:例如我們java中用到的wait()、await()方法。
喚醒操作:java中通常指notify()、signal()方法。阻塞和喚醒操作可以認為是一種同步與互斥方法,不同線程通過這兩種方法進行通信協作。
由於Java中使用的是MESA模型,下面我們只對MESA模型做詳細介紹,對於另外兩個只做簡單總結。我們先從Java的關鍵字synchronized介紹。由於使用的是MESA模型,它是沒有signalQueue的,而synchronized也僅支持一個條件變量。當然,JDK中還可以通過Lock實現多個條件變量,簡易synchronized中的MESA模型如下所示:
上面的執行過程如下:
- 多個線程進入到enterQueue,JVM保證只有一個線程能夠進入到管程內部,synchronize中進入管程的線程是隨機的。
- 通過條件變量判斷當前線程是否能執操作,若不能執行,則跳到第3步。若能執行,則跳到第4步。
- 條件變量調用wait()方法,當前線程進入阻塞狀態,將其放入varQueue,等待其它線程喚醒,跳到步驟1。
- 執行相應的操作,執行完畢后調用notify()或者notifyAll()方法,喚醒在varQueue中阻塞的一個或全部線程。
- 所有在varQueue中的線程會被放入enterQueue中,再次執行步驟1。
經過上述步驟,我們有一個思考,那么就是當前執行的線程喚醒在varQueue的線程的時候,這兩個線程間的執行順序是什么?這也是不同模型所要了解的重點。
MESA模型:條件變量隊列中的阻塞線程被喚醒后,不會立即執行而是放入到enterQueue隊列,等待下一次JVM的選擇運行。而正在運行的線程會繼續執行,直到程序執行完畢。
Hasen 模型:條件變量隊列中的阻塞線程被喚醒后,會在當前線程執行完成后立即運行剛被喚醒的阻塞線程,它的優先級是比enterQueue隊列中的線程優先級高的。
Hoare 模型:條件變量隊列中的阻塞線程被喚醒后,當前線程會立即中斷,並運行剛剛被喚醒的阻塞線程,等阻塞線程完成再回來運行。
(上述的Hasen、Hoare模型的模式從總結上來說是這樣的,但還是與實際有點不同,但是由於它不是Java用到的模型,我們也不去深究。如果有感興趣的可以到這個網站去了解:https://en.wikipedia.org/wiki/Monitor_(synchronization)#Blocking_condition_variables)
下面,我們可以通過一個簡單的阻塞隊列的實現方式來更近一步了解管程是怎么進行同步與互斥的。
1 package com.study.unit3.section2; 2 3 import java.util.LinkedList; 4 import java.util.List; 5 import java.util.concurrent.locks.Condition; 6 import java.util.concurrent.locks.ReentrantLock; 7 8 /** 9 * @author HILL 10 * @version V1.0 11 * @date 2019/7/26 12 **/ 13 public class BlockQueue { 14 15 private List<Integer> blockList = new LinkedList<Integer>(); 16 private final int limit; 17 //lock實現互斥 18 private ReentrantLock lock = new ReentrantLock(); 19 //條件變量實現同步通信 20 private Condition putCondition = lock.newCondition(); 21 private Condition getCondition = lock.newCondition(); 22 23 public BlockQueue(int limit) { 24 this.limit = limit; 25 } 26 27 public Integer get() { 28 lock.lock(); 29 try { 30 while (blockList.size() <= 0) { 31 //隊列中沒有元素,阻塞想要獲取元素的線程 32 getCondition.await(); 33 } 34 } catch (InterruptedException e) { 35 e.printStackTrace(); 36 } finally { 37 lock.unlock(); 38 } 39 //通知線程可以放入元素 40 putCondition.signalAll(); 41 return blockList.remove(0); 42 } 43 44 public void set(Integer num) { 45 lock.lock(); 46 try { 47 while (blockList.size() == limit) { 48 putCondition.await(); 49 } 50 blockList.add(num); 51 getCondition.signalAll(); 52 } catch (Exception e) { 53 e.printStackTrace(); 54 } finally { 55 lock.unlock(); 56 } 57 58 } 59 }
-
Java中wait()方法的正確使用
Java的管程是基於MESA模型的,那它的特點是什么?那就是此刻滿足執行條件,被激活的線程不一定能馬上執行,那就有可能存在一個問題。 下一次線程進入時條件已經不滿足了,那線程應該是不能執行的。所以,我們要在判斷條件的地方使用while判斷。至於詳細的原因,我們可以通過代碼來說明。
1 if(blockList.size()<=0){ 2 //線程不滿足條件,進入休眠。 3 getCondition.await(); //當下一次線程被激活的時候,線程會從這一步開始往下執行。 4 5 }
可以看到,當線程再次獲得執行權利的時候,是不用從方法口重新進入,而是從await()這個方法往下面走。因為MESA模型的策略,當線程再次獲得執行權利的時候,它的運行條件blockList.size()<=0不一定滿足,如果讓線程往下面執行,會發生意想不到的錯誤。所以,正確的方式應該是這樣的:
1 2 while (blockList.size() <= 0) { 3 //線程不滿足條件,進入休眠。 4 getCondition.await(); //當下一次線程被激活的時候,由於while的原因,會再一次判斷運行結果。 5 }
-
總結
對於JAVA的管程來說,具體的表現形式就是synchronize和lock接口。只有了解JAVA管程的具體策略才能了解並發程序中的線程的執行策略,當出錯的時候才能更加准確的分析出BUG。