
“怎么還慢吞吞的!”,JVM翹著二郎腿在調(diào)度室里大聲喊道?!癟hread-2,你已經(jīng)被我創(chuàng)建出來(lái)了,趕緊干活!”。

Thread-2這才反應(yīng)過(guò)來(lái),看看自己的身體,是個(gè)結(jié)構(gòu)分明、線條優(yōu)美的線程棧。先看看自己有啥東西:一個(gè)程序計(jì)數(shù)器、一連串的函數(shù)棧幀。程序計(jì)數(shù)器記錄程序執(zhí)行到哪里了,函數(shù)棧幀是一個(gè)方法的信息體,里面包含局部變量表、操作數(shù)棧等。

“好的,老板。我看到我有一個(gè)run方法,里面調(diào)用了produce()方法,用人類的話說(shuō),我應(yīng)該是個(gè)生產(chǎn)者啊”。JVM點(diǎn)了點(diǎn)頭,這小子挺機(jī)靈的。“那你趕緊去生產(chǎn)產(chǎn)品吧,消費(fèi)者線程打電話催了好幾次了”。


消費(fèi)者線程,那是個(gè)啥?
跟你一樣,也是由我創(chuàng)建出來(lái)的一個(gè)線程。只是職責(zé)與你不同,你負(fù)責(zé)生產(chǎn)產(chǎn)品,它負(fù)責(zé)消費(fèi)產(chǎn)品。

它在哪里,我怎么看不見(jiàn)。
你們彼此都是有獨(dú)立空間的,但是你們需要協(xié)作完成工作,你得注意兩點(diǎn),①操作臨界區(qū)數(shù)據(jù)時(shí)要進(jìn)行互斥(同一時(shí)刻只能有一個(gè)線程操作這個(gè)共享變量),②修改后你得通知其他線程可以操作這個(gè)共享變量了;
你叫Producer類,你有一個(gè)放在方法區(qū)的實(shí)例變量taskQueue(List<Integer> taskQueue),你往這個(gè)容器中放東西,消費(fèi)者線程的Consumer類也要持有同一個(gè)實(shí)例變量,這樣它們就可以從這個(gè)容器里取產(chǎn)品了。JVM怕它不懂,多解釋了幾句。
看老大停頓了,天性活潑多問(wèn)的Thread-2正準(zhǔn)備開(kāi)口接著問(wèn)...
“先調(diào)用produce()函數(shù)棧幀,走一遍指令再說(shuō)”。JVM沒(méi)搭理它,發(fā)出了指令。

過(guò)了1ms,活潑的Thread-2又打電話來(lái)了。我已經(jīng)走了一遍了,剛開(kāi)始讓我判斷是否生產(chǎn)到5個(gè)了,如果到了就調(diào)用wait()方法。因?yàn)閿?shù)量為0所以就繼續(xù)走下面的邏輯,生產(chǎn)了一個(gè)產(chǎn)品放在taskQueue中,然后調(diào)用了notify()方法。調(diào)用這兩個(gè)方法的時(shí)候,到底發(fā)生了什么???
你這大大咧咧的性格,怎么忘了描述最重要的那點(diǎn)了呢?
Thread-2摸摸自己紅撲撲的臉蛋,生怕老板罵它。
你剛開(kāi)始運(yùn)行的時(shí)候,系統(tǒng)是不是讓你去搶占taskQueue的monitor,你首先得搶到這個(gè)實(shí)例變量的monitor,你才能運(yùn)行。才有后面調(diào)用wait()或notify()的事。
這些都是synchronized在起作用,它的作用是在代碼塊前后加上monitorenter和monitorexit兩個(gè)字節(jié)碼指令,這樣就不會(huì)有其他線程同時(shí)操作taskQueue這個(gè)變量。這個(gè)就是我們常說(shuō)的互斥鎖。當(dāng)然synchronized可以加在方法上或者包裹一個(gè)代碼塊,那說(shuō)來(lái)就話長(zhǎng)了。
我想起來(lái)了,我在taskQueue的對(duì)象頭中確實(shí)看到了是我持有了鎖。執(zhí)行taskQueue.wait()或taskQueue.notify()方法的時(shí)候,是需要持有這個(gè)對(duì)象的monitor的。

是的,其中wait()、notify()這樣的方法是要在synchronized關(guān)鍵字包裹的中才能執(zhí)行的,否則會(huì)拋出IllegalMonitorStateException異常。
那這兩個(gè)方法的作用是啥咧?
你先生產(chǎn)到5個(gè)產(chǎn)品。
JVM剛準(zhǔn)備看會(huì)新聞,Thread-2來(lái)電了。我生產(chǎn)5個(gè)了,調(diào)用了wait()方法,釋放了monitor。我現(xiàn)在身子就好像僵硬了一樣,什么都干不了。JVM哈哈哈一笑,你這是把自己放在了一個(gè)等待池中,等待taskQueue實(shí)例變量的鎖。讓我跟你說(shuō)說(shuō)現(xiàn)在外面都發(fā)生了什么吧。
還記得你生產(chǎn)第一個(gè)產(chǎn)品的時(shí)候,就調(diào)用了notify()方法吧。這個(gè)方法會(huì)通知其他的線程來(lái)消費(fèi)產(chǎn)品。但是他們沒(méi)有進(jìn)來(lái)消費(fèi),因?yàn)槟氵€沒(méi)有釋放taskQueue的monitor,等到你生產(chǎn)滿5個(gè)后,會(huì)調(diào)用wait()方法,這個(gè)方法會(huì)讓你釋放掉monitor,并進(jìn)入等待池。
消費(fèi)者線程那邊也是同樣的邏輯,他們會(huì)在收到通知后去競(jìng)爭(zhēng)taskQueue的monitor,競(jìng)爭(zhēng)到的線程開(kāi)始進(jìn)入consume()方法,它同樣需要加互斥鎖。當(dāng)他們消費(fèi)完了之后,會(huì)調(diào)用wait()方法,你就有機(jī)會(huì)去爭(zhēng)奪taskQueue的monitor。搶到以后,你就可以繼續(xù)工作了。

最終的結(jié)果你可以看下:

Thread-0先運(yùn)行,發(fā)現(xiàn)集合為空,則進(jìn)入等待池。生產(chǎn)者生產(chǎn)產(chǎn)品,達(dá)到5個(gè)后進(jìn)入wait狀態(tài),釋放taskQueue的monitor,消費(fèi)者線程Thread-0獲得taskQueue的monitor進(jìn)入運(yùn)行狀態(tài)。以此類推消費(fèi)線程Thread-1的行為。
JVM似乎很中意這個(gè)剛出生的娃娃?!翱茨闾熨x異稟,老哥賜你錦囊一幅,當(dāng)你迷茫的時(shí)候,記得拿出來(lái)看看”。
錦囊第一法:搬完磚了怎么休息?
調(diào)用wait方法,它是Object類的方法,final native修飾,即本地方法,不可被子類重寫(xiě)!必須在sychronized塊中調(diào)用;
作用:當(dāng)前線程Thread T釋放對(duì)象鎖,并將自己放在等待池(wait set)中。直到其他線程獲取這個(gè)對(duì)象的monitor控制權(quán),以及發(fā)生以下四種情況之一時(shí),線程Thread T將被喚醒:
①其他線程持有同一個(gè)對(duì)象的monitor并調(diào)用notify()方法
②其他線程持有同一個(gè)對(duì)象的monitor并調(diào)用notifyAll()方法
③超過(guò)等待時(shí)間
④被其他線程打斷
調(diào)用wait方法的要點(diǎn):
- 當(dāng)前線程必須擁有這個(gè)對(duì)象的monitor,否則會(huì)拋出IllegalMonitorStateException異常;
- 當(dāng)前線程將自己放在wait set中,并解除所有在這個(gè)對(duì)象上的同步聲明;
- 當(dāng)前線程調(diào)用wait方法后返回,線程及對(duì)象的同步狀態(tài)與調(diào)用wait方法時(shí)是一致的;
- 當(dāng)被喚醒時(shí),會(huì)與其他線程一起競(jìng)爭(zhēng)獲取對(duì)象的同步權(quán)(獲取monitor);
怎么用?
線程的喚醒緣故不一定是上面提到的四種情況,有時(shí)候可能會(huì)是假喚醒狀態(tài),所以需要在輪詢+條件判斷的代碼塊中使用,在不滿足條件時(shí),讓線程一直等待:
synchronized (obj) {
while (condition does not hold)
obj.wait(timeout);
... // Perform action appropriate to condition
}
錦囊第二法:怎么通知其他線程小伙伴?
調(diào)用notify()或者notifyAll()方法。它們都是Object類的final native方法,必須在sychronized塊中調(diào)用;
作用:notify是喚醒等待相同對(duì)象monitor的其中一個(gè)線程Thread T(Thread T須調(diào)用了wait方法),至于是哪個(gè)線程被喚醒,這要取決于具體實(shí)現(xiàn),我們無(wú)法判斷。notifyAll是喚醒所有等待相同對(duì)象monitor的線程們。其他跟notify一樣。
Thread T不會(huì)立馬進(jìn)入運(yùn)行狀態(tài):
①當(dāng)前調(diào)用了notify()方法后,當(dāng)前線程不一定會(huì)立馬釋放對(duì)象的monitor,直到當(dāng)前線程的同步塊執(zhí)行完成后才會(huì)釋放;
②Thread T得到可以喚醒的通知,對(duì)象的monitor也被釋放了,此時(shí)會(huì)與其他等待當(dāng)前對(duì)象monitor的對(duì)象一同競(jìng)爭(zhēng)這個(gè)monitor,獲取到這個(gè)對(duì)象的monitor后方可進(jìn)入運(yùn)行狀態(tài)。
像這樣用就可以了:
synchronized(lockObject){
//establish_the_condition;
lockObject.notify();
//any additional code if needed
}
Thread-2就這樣慢慢熟悉了如何與其他線程進(jìn)行協(xié)作,保證數(shù)據(jù)安全地運(yùn)行,后來(lái)它了解到人類為了寫(xiě)出線程安全的代碼,需要考慮到原子性、可見(jiàn)性、有序性等??磥?lái)又有新東西可以學(xué)了。