Java監(jiān)視器支持兩種線程:互斥和協(xié)作。
前面我們介紹了采用對(duì)象鎖和重入鎖來(lái)實(shí)現(xiàn)的互斥。這一篇中,我們來(lái)看一看線程的協(xié)作。
舉個(gè)例子:有一家漢堡店舉辦吃漢堡比賽,決賽時(shí)有3個(gè)顧客來(lái)吃,3個(gè)廚師來(lái)做,一個(gè)服務(wù)員負(fù)責(zé)協(xié)調(diào)漢堡的數(shù)量。為了避免浪費(fèi),制作好的漢堡被放進(jìn)一個(gè)能裝有10個(gè)漢堡的長(zhǎng)條狀容器中,按照先進(jìn)先出的原則取漢堡。如果容器被裝滿,則廚師停止做漢堡,如果顧客發(fā)現(xiàn)容器內(nèi)的漢堡吃完了,就可以拍響容器上的鬧鈴,提醒廚師再做幾個(gè)漢堡出來(lái)。此時(shí)服務(wù)員過(guò)來(lái)安撫顧客,讓他等待。而一旦廚師的漢堡做出來(lái),就會(huì)讓服務(wù)員通知顧客,漢堡做好了,讓顧客繼續(xù)過(guò)來(lái)取漢堡。
這里,顧客其實(shí)就是我們所說(shuō)的消費(fèi)者,而廚師就是生產(chǎn)者。容器是決定廚師行為的監(jiān)視器,而服務(wù)員則負(fù)責(zé)監(jiān)視顧客的行為。
在JVM中,此種監(jiān)視器被稱為等待并喚醒監(jiān)視器。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
在這種監(jiān)視器中,一個(gè)已經(jīng)持有該監(jiān)視器的線程,可以通過(guò)調(diào)用監(jiān)視對(duì)象的wait方法,暫停自身的執(zhí)行,并釋放監(jiān)視器,自己進(jìn)入一個(gè)等待區(qū),直到監(jiān)視器內(nèi)的其他線程調(diào)用了監(jiān)視對(duì)象的notify方法。當(dāng)一個(gè)線程調(diào)用喚醒命令以后,它會(huì)持續(xù)持有監(jiān)視器,直到它主動(dòng)釋放監(jiān)視器。而這之后,等待線程會(huì)蘇醒,其中的一個(gè)會(huì)重新獲得監(jiān)視器,判斷條件狀態(tài),以便決定是否繼續(xù)進(jìn)入等待狀態(tài)或者執(zhí)行監(jiān)視區(qū)域,或者退出。
請(qǐng)看下面的代碼:
1. public class NotifyTest {
2. private String flag = "true";
3.
4. class NotifyThread extends Thread{
5. public NotifyThread(String name) {
6. super(name);
7. }
8. public void run() {
9. try {
10. sleep(3000);//推遲3秒鐘通知
11. } catch (InterruptedException e) {
12. e.printStackTrace();
13. }
14.
15. flag = "false";
16. flag.notify();
17. }
18. };
19.
20. class WaitThread extends Thread {
21. public WaitThread(String name) {
22. super(name);
23. }
24.
25. public void run() {
26.
27. while (flag!="false") {
28. System.out.println(getName() + " begin waiting!");
29. long waitTime = System.currentTimeMillis();
30. try {
31. flag.wait();
32. } catch (InterruptedException e) {
33. e.printStackTrace();
34. }
35. waitTime = System.currentTimeMillis() - waitTime;
36. System.out.println("wait time :"+waitTime);
37. }
38. System.out.println(getName() + " end waiting!");
39.
40. }
41. }
42.
43. public static void main(String[] args) throws InterruptedException {
44. System.out.println("Main Thread Run!");
45. NotifyTest test = new NotifyTest();
46. NotifyThread notifyThread =test.new NotifyThread("notify01");
47. WaitThread waitThread01 = test.new WaitThread("waiter01");
48. WaitThread waitThread02 = test.new WaitThread("waiter02");
49. WaitThread waitThread03 = test.new WaitThread("waiter03");
50. notifyThread.start();
51. waitThread01.start();
52. waitThread02.start();
53. waitThread03.start();
54. }
55.
56. }
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
這段代碼啟動(dòng)了三個(gè)簡(jiǎn)單的wait線程,當(dāng)他們處于等待狀態(tài)以后,試圖由一個(gè)notify線程來(lái)喚醒。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
運(yùn)行這段程序,你會(huì)發(fā)現(xiàn),滿屏的java.lang.IllegalMonitorStateException,根本不是你想要的結(jié)果。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
請(qǐng)注意以下幾個(gè)事實(shí):
1. 任何一個(gè)時(shí)刻,對(duì)象的控制權(quán)(monitor)只能被一個(gè)線程擁有。
2. 無(wú)論是執(zhí)行對(duì)象的wait、notify還是notifyAll方法,必須保證當(dāng)前運(yùn)行的線程取得了該對(duì)象的控制權(quán)(monitor)。
3. 如果在沒有控制權(quán)的線程里執(zhí)行對(duì)象的以上三種方法,就會(huì)報(bào)java.lang.IllegalMonitorStateException異常。
4. JVM基于多線程,默認(rèn)情況下不能保證運(yùn)行時(shí)線程的時(shí)序性。
也就是說(shuō),當(dāng)線程在調(diào)用某個(gè)對(duì)象的wait或者notify方法的時(shí)候,要先取得該對(duì)象的控制權(quán),換句話說(shuō),就是進(jìn)入這個(gè)對(duì)象的監(jiān)視器。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
通過(guò)前面對(duì)同步的討論,我們知道,要讓一個(gè)線程進(jìn)入某個(gè)對(duì)象的監(jiān)視器,通常有三種方法:
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
1: 執(zhí)行對(duì)象的某個(gè)同步實(shí)例方法
2: 執(zhí)行對(duì)象對(duì)應(yīng)的同步靜態(tài)方法
3: 執(zhí)行對(duì)該對(duì)象加同步鎖的同步塊
顯然,在上面的例程中,我們用第三種方法比較合適。
于是我們將上面的wait和notify方法調(diào)用包在同步塊中。
1. synchronized (flag) {
2. flag = "false";
3. flag.notify();
4. }
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
1. synchronized (flag) {
2. while (flag!="false") {
3. System.out.println(getName() + " begin waiting!");
4. long waitTime = System.currentTimeMillis();
5. try {
6. flag.wait();
7. } catch (InterruptedException e) {
8. e.printStackTrace();
9. }
10. waitTime = System.currentTimeMillis() - waitTime;
11. System.out.println("wait time :"+waitTime);
12. }
13. System.out.println(getName() + " end waiting!");
14. }
但是,運(yùn)行這個(gè)程序,我們發(fā)現(xiàn)事與愿違。那個(gè)非法監(jiān)視器異常又出現(xiàn)了。。。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
我們注意到,針對(duì)flag的同步塊中,我們實(shí)際上已經(jīng)更改了flag對(duì)對(duì)象的引用: flag="false";
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
顯然,這樣一來(lái),同步塊也無(wú)能為力了,因?yàn)槲覀兏静皇轻槍?duì)唯一的一個(gè)對(duì)象在進(jìn)行同步。
我們不妨將flag封裝到JavaBean或者數(shù)組中去,這樣用JavaBean對(duì)象或者數(shù)組對(duì)象進(jìn)行同步,就可以達(dá)到既能修改里面參數(shù)又不耽誤同步的目的。
1. private String flag[] = {"true"};
1. synchronized (flag) {
2. flag[0] = "false";
3. flag.notify();
4. }
1. synchronized (flag) {
2. flag[0] = "false";
3. flag.notify();
4. }synchronized (flag) {
5. while (flag[0]!="false") {
6. System.out.println(getName() + " begin waiting!");
7. long waitTime = System.currentTimeMillis();
8. try {
9. flag.wait();
10.
11. } catch (InterruptedException e) {
12. e.printStackTrace();
13. }
運(yùn)行這個(gè)程序,看不到異常了。但是仔細(xì)觀察結(jié)果,貌似只有一個(gè)線程被喚醒。利用jconsole等工具查看線程狀態(tài),發(fā)現(xiàn)的確還是有兩個(gè)線程被阻塞的。這是為啥呢?
程序中使用了flag.notify()方法。只能是隨機(jī)的喚醒一個(gè)線程。我們可以改用flag.notifyAll()方法。這樣,所有被阻塞的線程都會(huì)被喚醒了。
最終代碼請(qǐng)讀者自己修改,這里不再贅述。
好了,親愛的讀者們,讓我們回到開篇提到的漢堡店大賽問(wèn)題當(dāng)中去,來(lái)看一看廚師、服務(wù)生和顧客是怎么協(xié)作進(jìn)行這個(gè)比賽的。
首先我們構(gòu)造故事中的三個(gè)次要對(duì)象:漢堡包、存放漢堡包的容器、服務(wù)生
public class Waiter {//服務(wù)生,這是個(gè)配角,不需要屬性。
}
class Hamberg {
//漢堡包
private int id;//漢堡編號(hào)
private String cookerid;//廚師編號(hào)
public Hamberg(int id, String cookerid){
this.id = id;
this.cookerid = cookerid;
System.out.println(this.toString()+"was made!");
}
@Override
public String toString() {
return "#"+id+" by "+cookerid;
}
}
class HambergFifo {
//漢堡包容器
List<Hamberg> hambergs = new ArrayList<Hamberg>();//借助ArrayList來(lái)存放漢堡包
int maxSize = 10;//指定容器容量
//放入漢堡
public <T extends Hamberg> void push(T t) {
hambergs.add(t);
}
//取出漢堡
public Hamberg pop() {
Hamberg h = hambergs.get(0);
hambergs.remove(0);
return h;
}
//判斷容器是否為空
public synchronized boolean isEmpty() {
return hambergs.isEmpty();
}
//判斷容器內(nèi)漢堡的個(gè)數(shù)
public synchronized int size() {
return hambergs.size();
}
//返回容器的最大容量
public synchronized int getMaxSize() {
return this.maxSize;
}
//判斷容器是否已滿,未滿為真
public synchronized boolean isNotFull(){
return hambergs.size() < this.maxSize;
}
}
接下來(lái)我們構(gòu)造廚師對(duì)象:
class Cooker implements Runnable {
//廚師要面對(duì)容器
HambergFifo pool;
//還要面對(duì)服務(wù)生
Waiter waiter;
public Cooker(Waiter waiter, HambergFifo hambergStack) {
this.pool = hambergStack;
this.waiter = waiter;
}
//制造漢堡
public void makeHamberg() {
//制造的個(gè)數(shù)
int madeCount = 0;
//因?yàn)槿萜鳚M,被迫等待的次數(shù)
int fullFiredCount = 0;
try {
while (true) {
//制作漢堡前的準(zhǔn)備工作
Thread.sleep(1000);
if (pool.isNotFull()) {
synchronized (waiter) {
//容器未滿,制作漢堡,并放入容器。
pool.push(new Hamberg(++madeCount,Thread.currentThread().getName()));
//說(shuō)出容器內(nèi)漢堡數(shù)量
System.out.println(Thread.currentThread().getName() + ": There are "
+ pool.size() + " Hambergs in all");
//讓服務(wù)生通知顧客,有漢堡可以吃了
waiter.notifyAll();
System.out.println("### Cooker: waiter.notifyAll() :"+
" Hi! Customers, we got some new Hambergs!");
}
} else {
synchronized (pool) {
if (fullFiredCount++ < 10) {
//發(fā)現(xiàn)容器滿了,停止做漢堡的嘗試。
System.out.println(Thread.currentThread().getName() +
": Hamberg Pool is Full, Stop making hamberg");
System.out.println("### Cooker: pool.wait()");
//漢堡容器的狀況使廚師等待
pool.wait();
} else {
return;
}
}
}
//做完漢堡要進(jìn)行收尾工作,為下一次的制作做準(zhǔn)備。
Thread.sleep(1000);
}
} catch (Exception e) {
madeCount--;
e.printStackTrace();
}
}
public void run() {
makeHamberg();
}
}
接下來(lái),我們構(gòu)造顧客對(duì)象:
class Customer implements Runnable {
//顧客要面對(duì)服務(wù)生
Waiter waiter;
//也要面對(duì)漢堡包容器
HambergFifo pool;
//想要記下自己吃了多少漢堡
int ateCount = 0;
//吃每個(gè)漢堡的時(shí)間不盡相同
long sleeptime;
//用于產(chǎn)生隨機(jī)數(shù)
Random r = new Random();
public Customer(Waiter waiter, HambergFifo pool) {
this.waiter = waiter;
this.pool = pool;
}
public void run() {
while (true) {
try {
//取漢堡
getHamberg();
//吃漢堡
eatHamberg();
} catch (Exception e) {
synchronized (waiter) {
System.out.println(e.getMessage());
//若取不到漢堡,要和服務(wù)生打交道
try {
System.out.println("### Customer: waiter.wait():"+
" Sorry, Sir, there is no hambergs left, please wait!");
System.out.println(Thread.currentThread().getName()
+ ": OK, Waiting for new hambergs");
//服務(wù)生安撫顧客,讓他等待。
waiter.wait();
continue;
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
}
private void eatHamberg() {
try {
//吃每個(gè)漢堡的時(shí)間不等
sleeptime = Math.abs(r.nextInt(3000)) * 5;
System.out.println(Thread.currentThread().getName()
+ ": I'm eating the hamberg for " + sleeptime + " milliseconds");
Thread.sleep(sleeptime);
} catch (Exception e) {
e.printStackTrace();
}
}
private void getHamberg() throws Exception {
Hamberg hamberg = null;
synchronized (pool) {
try {
//在容器內(nèi)取漢堡
hamberg = pool.pop();
ateCount++;
System.out.println(Thread.currentThread().getName()
+ ": I Got " + ateCount + " Hamberg " + hamberg);
System.out.println(Thread.currentThread().getName()
+ ": There are still " + pool.size() + " hambergs left");
} catch (Exception e) {
pool.notifyAll();
System.out.println("### Customer: pool.notifyAll()");
throw new Exception(Thread.currentThread().getName() +
": OH MY GOD!!!! No hambergs left, Waiter![Ring the bell besides the hamberg pool]");
}
}
}
}
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
最后,我們構(gòu)造漢堡店,讓這個(gè)故事發(fā)生:
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
public class HambergShop {
Waiter waiter = new Waiter();
HambergFifo hambergPool = new HambergFifo();
Customer c1 = new Customer(waiter, hambergPool);
Customer c2 = new Customer(waiter, hambergPool);
Customer c3 = new Customer(waiter, hambergPool);
Cooker cooker = new Cooker(waiter, hambergPool);
public static void main(String[] args) {
HambergShop hambergShop = new HambergShop();
Thread t1 = new Thread(hambergShop.c1, "Customer 1");
Thread t2 = new Thread(hambergShop.c2, "Customer 2");
Thread t3 = new Thread(hambergShop.c3, "Customer 3");
Thread t4 = new Thread(hambergShop.cooker, "Cooker 1");
Thread t5 = new Thread(hambergShop.cooker, "Cooker 2");
Thread t6 = new Thread(hambergShop.cooker, "Cooker 3");
t4.start();
t5.start();
t6.start();
try {
Thread.sleep(10000);
} catch (Exception e) {
}
t1.start();
t2.start();
t3.start();
}
}
運(yùn)行這個(gè)程序吧,然后你會(huì)看到我們漢堡店的比賽進(jìn)行的很好,只是不知道那些顧客是不是會(huì)被撐到。。。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
讀到這里,有的讀者可能會(huì)想到前面介紹的重入鎖ReentrantLock。
有的讀者會(huì)問(wèn):如果我用ReentrantLock來(lái)代替上面這些例程當(dāng)中的
synchronized塊,是不是也可以呢?感興趣的讀者不妨一試。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
但是在這里,我想提前給出結(jié)論,就是,
如果用ReentrantLock的lock()和unlock()方法代替上面的synchronized塊,那么上面這些程序還是要拋出java.lang.IllegalMonitorStateException異常的,不僅如此,你甚至還會(huì)看到線程死鎖。原因就是當(dāng)某個(gè)線程調(diào)用第三方對(duì)象的wait或者notify方法的時(shí)候,并沒有進(jìn)入第三方對(duì)象的監(jiān)視器,于是拋出了異常信息。但此時(shí),程序流程如果沒有用finally來(lái)處理unlock方法,那么你的線程已經(jīng)被lock方法上鎖,并且無(wú)法解鎖。程序在java.util.concurrent框架的語(yǔ)義級(jí)別死鎖了,你用JConsole這種工具來(lái)檢測(cè)JVM死鎖,還檢測(cè)不出來(lái)。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
正確的做法就是,只使用ReentrantLock,而不使用wait或者notify方法。因?yàn)镽eentrantLock已經(jīng)對(duì)這種互斥和協(xié)作進(jìn)行了概括。所以,根據(jù)你程序的需要,請(qǐng)單獨(dú)采用重入鎖或者synchronized一種同步機(jī)制,最好不要混用。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
好了,我們現(xiàn)在明白:
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
1. 線程的等待或者喚醒,并不是讓線程調(diào)用自己的wait或者notify方法,而是通過(guò)調(diào)用線程共享對(duì)象的wait或者notify方法來(lái)實(shí)現(xiàn)。
2. 線程要調(diào)用某個(gè)對(duì)象的wait或者notify方法,必須先取得該對(duì)象的監(jiān)視器。
3. 線程的協(xié)作必須以線程的互斥為前提,這種協(xié)作實(shí)際上是一種互斥下的協(xié)作。
轉(zhuǎn)載注明出處:http://x-
spirit.javaeye.com/、http:
//www.tkk7.com/zhangwei217245/
下一講當(dāng)中,我們來(lái)看看如何實(shí)實(shí)在在的解決線程之間搶占共享資源的問(wèn)題。敬請(qǐng)期待!