1. 程式人生 > >Java多執行緒--Monitor物件(一)

Java多執行緒--Monitor物件(一)

1. 什麼是Monitor?

    Monitor其實是一種同步工具,也可以說是一種同步機制,它通常被描述為一個物件,主要特點是:

  • 物件的所有方法都被“互斥”的執行。好比一個Monitor只有一個執行“許可”,任一個執行緒進入任何一個方法都需要獲得這個“許可”,離開時把許可歸還。
  • 通常提供singal機制:允許正持有“許可”的執行緒暫時放棄“許可”,等待某個謂詞成真(條件變數),而條件成立後,當前程序可以“通知”正在等待這個條件變數的執行緒,讓他可以重新去獲得執行許可。

    Monitor物件可以被多執行緒安全地訪問。關於“互斥”與“為什麼要互斥”,我就不傻X兮兮解釋了;而關於Monitor的singal機制,歷史上曾經出現過兩大門派,分別是Hoare派和Mesa派(上過海波老師OS課的SS同學應該對這個有印象),我還是用我的理解通俗地庸俗地解釋一下:

  • Hoare派的singal機制江湖又稱“Blocking condition variable”,特點是,當“發通知”的執行緒發出通知後,立即失去許可,並“親手”交給等待者,等待者執行完畢後再將許可交還通知者。在這種機制裡,可以等待者拿到許可後,謂詞肯定為真——也就是說等待者不必再次檢查條件成立與否,所以對條件的判斷可以使用“if”,不必“while”
  • Mesa派的signal機制又稱“Non-Blocking condition variable”, 與Hoare不同,通知者發出通知後,並不立即失去許可,而是把聞風前來等待者安排在ready queue裡,等到schedule時有機會去拿到“許可”。這種機制裡,等待者拿到許可後不能確定在這個時間差裡是否有別的等待者進入過Monitor,因此不能保證謂詞一定為真,所以對條件的判斷必須使用“while”

    這兩種方案可以說各有利弊,但Mesa派在後來的盟主爭奪中漸漸佔了上風,被大多數實現所採用,有人給這種signal另外起了個別名叫“notify”,想必你也知道,Java採取的就是這個機制。

2. Monitor與Java不得不說的故事

    子曰:“Java物件是天生的Monitor。”每一個Java物件都有成為Monitor的“潛質”。這是為什麼?因為在Java的設計中,每一個物件自打孃胎裡出來,就帶了一把看不見的鎖,通常我們叫“內部鎖”,或者“Monitor鎖”,或者“Intrinsic lock”。為了裝逼起見,我們就叫它Intrinsic lock吧。有了這個鎖的幫助,只要把類的所有物件方法都用synchronized關鍵字修飾,並且所有域都為私有(也就是隻能通過方法訪問物件狀態),就是一個貨真價實的Monitor了。比如,我們舉一個大俗例吧:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Account {
	private int balance;
	
	public Account(int balance) {
		this.balance = balance;
	}
	
	synchronized public boolean withdraw(int amount){
		if(balance<amount)
			return false;
		balance -= amount;
		return true;
	}
	
	synchronized public void deposit(int amount){
		balance +=amount;
	}
	
}

3. synchronized關鍵字

    上面我們已經看到synchronized的一種用法,用來修飾方法,表示進入該方法需要對Intrinsic lock加鎖,離開時放鎖。synchronized可以用在程式塊中,顯示說明對“哪個物件的Intrinsic lock加鎖”,比如

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
synchronized public void deposit(int amount){
	balance +=amount;
}
// 等價於
public void deposit(int amount){
	synchronized(this){
		balance +=amount;
	}
}

    這時,你可能就要問了,你不是說任何物件都有intrinsic lock麼?而synchronized關鍵字又可以顯示指定去鎖誰,那我們是不是可以這樣做:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Account {
	private int balance;
	private Object lock = new Object();
	
	public Account(int balance) {
		this.balance = balance;
	}
	
	public boolean withdraw(int amount){
		synchronized (lock) {
			if(balance<amount)
				return false;
			balance -= amount;
			return true;
		}	
	}
	
	public void deposit(int amount){
		synchronized (lock) {
			balance +=amount;
		}		
	}
}

    不用this的內部鎖,而是用另外任意一個物件的內部鎖來完成完全相同的任務?沒錯,完全可以。不過,需要注意的是,這時候,你實際上禁止了“客戶程式碼加鎖”的行為。前幾天BBS上簡哥有一貼提到的bug其實就是這個,這個時候使用這份程式碼的客戶程式如果想當然地認為Account的同步是基於其內部鎖的,並且傻X兮兮地寫了類似下面的程式碼:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
	public static void main(String[] args) {
		Account account =new Account(1000);
		
		//some threads modifying account through Account’s methods...
		
		synchronized (account) {
			;//blabla
		}
	}

自認為後面的同步快對account加了鎖,期間的操作不會被其餘通過Account方法操作account物件的執行緒所幹擾,那就太悲劇了。因為他們並不相干,鎖住了不同的鎖。

4. Java中的條件變數

    正如我們前面所說,Java採取了wait/notify機制來作為intrinsic lock 相關的條件變數,表示為等待某一條件成立的條件佇列——說到這裡順帶插一段,條件佇列必然與某個鎖相關,並且語義上關聯某個謂詞(條件佇列、鎖、條件謂詞就是吉祥的一家)。所以,在使用wait/notify方法時,必然是已經獲得相關鎖了的,在進一步說,一個推論就是“wait/notify  方法只能出現在相應的同步塊中”。如果不呢?就像下面一段(notify表示的謂詞是“帳戶裡有錢啦~”):

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
	public void deposit(int amount){
		balance +=amount;
		notify();
	}

//或者這樣:

	public void deposit(int amount){
		synchronized (lock) {
			balance +=amount;
			notify();
		}
	}

這兩段都是錯的,第一段沒有在同步塊裡,而第二段拿到的是lock的內部鎖,呼叫的卻是this.notify(),讓人遺憾。執行時他們都會拋IllegalMonitorStateException異常——唉,想前一陣我參加一次筆試的時候,有一道題就是這個,讓你選所給程式碼會拋什麼異常,我當時就傻了,想這考得也太偏了吧,現在看看,確實是很基本的概念,當初被虐是壓根沒有理解wait/notify機制的緣故。那怎麼寫是對的呢?

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
	public void deposit(int amount){
		synchronized (lock) {
			balance +=amount;
			lock.notify();
		}
	}
//或者(取決於你採用的鎖):
	synchronized public void deposit(int amount){
		balance +=amount;
		notify();
	}

5.這就夠了嗎?

    看上去,Java的內部鎖和wait/notify機制已經可以滿足任何同步需求了,不是嗎?em…可以這麼說,但也可以說,不那麼完美。有兩個問題:

  • 鎖不夠用

    有時候,我們的類裡不止有一個狀態,這些狀態是相互獨立的,如果只用同一個內部鎖來維護他們全部,未免顯得過於笨拙,會嚴重影響吞吐量。你馬上會說,你剛才不是演示了用任意一個Object來做鎖嗎?我們多整幾個Object分別加鎖不就行了嗎?沒錯,是可行的。但這樣可能顯得有些醜陋,而且Object來做鎖本身就有語義不明確的缺點。

  • 條件變數不夠用

    Java用wait/notify機制實際上預設給一個內部鎖綁定了一個條件佇列,但是,有時候,針對一個狀態(鎖),我們的程式需要兩個或以上的條件佇列,比如,剛才的Account例子,如果某個2B銀行有這樣的規定“一個賬戶存款不得多於10000元”,這個時候,我們的存錢需要滿足“餘額+要存的數目不大於10000,否則等待,直到滿足這個限制”,取錢需要滿足“餘額足夠,否則等待,直到有錢為止”,這裡需要兩個條件佇列,一個等待“存款不溢位”,一個等待“存款足夠”,這時,一個預設的條件佇列夠用麼?你可能又說,夠用,我們可以模仿network裡的“多路複用”,一個佇列就能當多個來使,像這樣:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Account {
	public static final int BOUND = 10000;
	private int balance;
	
	public Account(int balance) {
		this.balance = balance;
	}
	
	synchronized public boolean withdraw(int amount) throws InterruptedException{
			while(balance<amount)
				wait();// no money, wait
			balance -= amount;
			notifyAll();// not full, notify
			return true;
	}
	
	synchronized public void deposit(int amount) throws InterruptedException{
			while(balance+amount >BOUND)
				wait();//full, wait
			balance +=amount;
			notifyAll();// has money, notify
	}
}

    不是挺好嗎?恩,沒錯,是可以。但是,仍然存在效能上的缺陷:每次都有多個執行緒被喚醒,而實際只有一個會執行,頻繁的上下文切換和鎖請求是件很廢的事情。我們能不能不要notifyAll,而每次只用notify(只喚醒一個)呢?不好意思,想要“多路複用”,就必須notifyAll,否則會有丟失訊號之虞(不解釋了)。只有滿足下面兩個條件,才能使用notify:

一,只有一個條件謂詞與條件佇列相關,每個執行緒從wait返回執行相同的邏輯。

二,一進一出:一個對條件變數的通知,語義上至多隻啟用一個執行緒。

    我又想插播一段:剛才寫上面那段程式碼,IDE提示拋InterruptedException,我想提一下,這是因為wait是一個阻塞方法,幾乎所有阻塞方法都會宣告可能拋InterruptedException,這是和Java的interrupt機制有關的,以後我們有機會再說。

    既然這麼做不優雅不高效不亞克西,那如之奈何?Java提供了其他工具嗎?是的。這就是傳說中的java.util.concurrency包裡的故事,今天也不說了,有機會在和大家討論。

主要參考資料:

1. Wiki

2. Addison Wesley, Java Concurrency in Practice ,Brian Goetz