Java线程的挂起与恢复 wait(), notify()方法介绍
一, 什么是線程的掛起與恢復(fù)
從字面理解也很簡單.
所謂線程掛起就是指暫停線程的執(zhí)行(阻塞狀態(tài)).
而恢復(fù)時(shí)就是讓暫停的線程得以繼續(xù)執(zhí)行.(返回就緒狀態(tài))
二, 為何需要掛起和恢復(fù)線程.
我們來看1個(gè)經(jīng)典的例子(生產(chǎn)消費(fèi)):
1個(gè)倉庫最多容納6個(gè)產(chǎn)品, 制造者現(xiàn)在需要制造超過20件產(chǎn)品存入倉庫, 銷售者要從倉庫取出這20件產(chǎn)品來消費(fèi).
制造和消費(fèi)的速度很可能是不一樣的, 編程實(shí)現(xiàn)兩者的同步.
1. 把倉庫設(shè)為1個(gè)容器, 容量為6
2. 把生產(chǎn) 和 消費(fèi)設(shè)為兩線程.
3. 無論生產(chǎn)還是消費(fèi), 每次的個(gè)數(shù)都是1.
4. 生產(chǎn)線程往倉庫添加20個(gè)產(chǎn)品, 消費(fèi)線程從倉庫取20個(gè)產(chǎn)品.
5. 倉庫滿時(shí), 生產(chǎn)線程必須暫停, 倉庫空時(shí), 消費(fèi)必須暫停.
可見, 為了滿足第5個(gè)條件, 線程必須有暫停和恢復(fù)執(zhí)行的功能. 這就是需要掛起和恢復(fù)線程的原因.
其實(shí)這個(gè)問題還有1個(gè)隱藏條件:
因?yàn)橛?個(gè)線程同時(shí)訪問修改同1個(gè)數(shù)據(jù)(容器), 所以生產(chǎn)和消費(fèi)線程的關(guān)鍵代碼必須是互斥的.
亦即系講, 當(dāng)生產(chǎn)線程訪問和修改容器時(shí), 恢復(fù)線程就必須阻塞, 否則數(shù)據(jù)會(huì)出錯(cuò).
三, 生產(chǎn)消費(fèi)問題的簡單代碼(不考慮掛起恢復(fù))
我們一步一步利用java代碼來實(shí)現(xiàn)這個(gè)問題.
3.1 產(chǎn)品
我們只需要定義1個(gè)類來描述產(chǎn)品.
本例中的產(chǎn)品只有1個(gè)屬性id (int 類型)用于區(qū)分.
代碼:
class Prod_1{private int id;public int getId(){return this.id;}public Prod_1(int id){this.id = id;} }3.2 倉庫(容器)
上面說過了, 倉庫的數(shù)據(jù)模型,實(shí)際上就是1個(gè)容器.
編程中常用的容器無非是棧和隊(duì)列. (每次進(jìn)出的個(gè)數(shù)都只能是1)
而在倉庫管理中, 一般會(huì)優(yōu)先把生產(chǎn)日期早的產(chǎn)品出庫(先進(jìn)先出), 所以這里就用隊(duì)列來實(shí)現(xiàn)了
例子中的隊(duì)列是1個(gè)靜態(tài)(數(shù)組)隊(duì)列.
靜態(tài)隊(duì)列的代碼實(shí)現(xiàn)其實(shí)并不容易理解. 如果有興趣的話可以看看本人關(guān)于靜態(tài)隊(duì)列的介紹: http://blog.csdn.net/nvd11/article/details/8816699
但在這里, 只需要明白兩個(gè)方法作用就ok了
1. public synchronized void enqueue(object ob)
作用是把ob放入隊(duì)列中, 也就是入庫.
2. public synchronized ob deQueue()
把隊(duì)列中最早放入的元素(返回值)拿出來, 也就是出庫.
注意上面兩個(gè)方法是同步的, 也就是互斥的.
代碼:
class ProdQueue_1{private Prod_1[] prod_q;private int pRear;private int pFront;public ProdQueue_1(int len){prod_q = new Prod_1[len + 1]; //array queue. set the max length = capacity + 1pRear = 0;pFront = 0;}public int getLen(){return (pRear - pFront + prod_q.length) % prod_q.length;}public boolean isEmpty(){return (pRear == pFront);}public boolean isFull(){return ((pRear + 1) % prod_q.length == pFront); } public synchronized void enQueue(Prod_1 p){if (this.isFull()){throw new RuntimeException("the warehouse is full!");}prod_q[pRear] = p;pRear = ((pRear + 1) + prod_q.length) % prod_q.length;} public synchronized Prod_1 deQueue(){if (this.isEmpty()){throw new RuntimeException("the warehouse is empty!");} =int p = pFront;pFront = ((pFront + 1) + prod_q.length) % prod_q.length; return prod_q[p];} public String toString(){if (this.isEmpty()){return "warehose: empty!";}StringBuffer s = new StringBuffer("count is " + this.getLen() + ": "); int i;for (i=pFront; i != pRear; ){s = s.append(prod_q[i].getId() + ",");i = ((i+1) + prod_q.length) % prod_q.length;}s = s.delete(s.length() - 1,s.length());return s.toString();} }3.3 生產(chǎn)線程
生產(chǎn)線程無非就是不斷調(diào)用容器類的入列函數(shù)(enQueue),把產(chǎn)品不斷放入容器中.
當(dāng)然要接受上面容器類作為1個(gè)成員.
而且因?yàn)槭蔷€程類, 必須實(shí)現(xiàn)Runnable接口.
代碼如下:
class Producer_1 implements Runnable{private ProdQueue_1 pq;private int count;public Producer_1(ProdQueue_1 pq, int count){this.pq = pq;this.count = count;}private void thrdSleep(int ms){try{Thread.sleep(ms);}catch(Exception e){}}public void run(){Prod_1 p;int i;for (i=0; i<count; i++){this.thrdSleep(1000);p = new Prod_1(i);pq.enQueue(p);System.out.printf("Producer: made the product %d\n", p.getId());}} }上面就是生產(chǎn)者的類, 構(gòu)造方法中有兩個(gè)參數(shù), 分別對(duì)應(yīng)其兩個(gè)成員:
1個(gè)就是容器的對(duì)象,? 另1個(gè)就是要生產(chǎn)的產(chǎn)品數(shù)量.
注意, 我在run()方法的循環(huán)中加了1個(gè)sleep()方法, 代表每生產(chǎn)1個(gè)產(chǎn)品停頓1秒(設(shè)置生產(chǎn)速度)
3.4 銷售線程
銷售線程的業(yè)務(wù)就是不斷地從容器中取出產(chǎn)品. 就是執(zhí)行容器對(duì)象的deQueue()方法了.
具體實(shí)現(xiàn)方法跟生產(chǎn)線程是類似的. 代碼如下:
class Seller_1 implements Runnable{private ProdQueue_1 pq;public Seller_1(ProdQueue_1 pq){this.pq = pq;}private void thrdSleep(int ms){try{Thread.sleep(ms);}catch(Exception e){}}public void run(){Prod_1 p;while(true){this.thrdSleep(2000);p = pq.deQueue();System.out.printf("Seller: sold the product %d\n", p.getId());}}}在銷售線程中, 每個(gè)循環(huán)利用sleep()方法停頓2秒, 也就設(shè)置了銷售速度是比生產(chǎn)速度慢一倍的.
3.5 啟動(dòng)類
國際慣例, 在1個(gè)啟動(dòng)類的靜態(tài)方法中,調(diào)用上面寫的業(yè)務(wù)類.
public class Td_prod_1{public static void f(){ProdQueue_1 pq = new ProdQueue_1(6);Producer_1 producer = new Producer_1(pq,20);Seller_1 seller = new Seller_1(pq);Thread thrd_prod = new Thread(producer);thrd_prod.start();Thread thrd_sell = new Thread(seller);thrd_sell.start();} }邏輯很簡單, 無非是定義1個(gè)容器對(duì)象.
然后利用這個(gè)容器對(duì)象構(gòu)造1個(gè)生產(chǎn)線程對(duì)象和1個(gè)銷售線程對(duì)象.
最后啟動(dòng)這個(gè)兩個(gè)線程.
3.6 執(zhí)行結(jié)果
執(zhí)行結(jié)果如下:
Producer: made the product 0 Seller: sold the product 0 Producer: made the product 1 Producer: made the product 2 Seller: sold the product 1 Producer: made the product 3 Producer: made the product 4 Seller: sold the product 2 Producer: made the product 5 Producer: made the product 6 Seller: sold the product 3 Producer: made the product 7 Producer: made the product 8 Seller: sold the product 4 Producer: made the product 9 Producer: made the product 10 Seller: sold the product 5 Producer: made the product 11 Exception in thread "Thread-0" java.lang.RuntimeException: the warehouse is full!at Thread_kng.Td_wait_notify.ProdQueue_1.enQueue(Td_prod_1.java:38)at Thread_kng.Td_wait_notify.Producer_1.run(Td_prod_1.java:92)at java.lang.Thread.run(Thread.java:722) Seller: sold the product 6 Seller: sold the product 7 Seller: sold the product 8 Seller: sold the product 9 Seller: sold the product 10 Seller: sold the product 11 Exception in thread "Thread-1" java.lang.RuntimeException: the warehouse is empty!at Thread_kng.Td_wait_notify.ProdQueue_1.deQueue(Td_prod_1.java:47)at Thread_kng.Td_wait_notify.Seller_1.run(Td_prod_1.java:118)at java.lang.Thread.run(Thread.java:722)可見到 結(jié)果中:
1開始, 生產(chǎn)線程和銷售線程是正常執(zhí)行的, 因?yàn)樗俣鹊牟煌? 大概生產(chǎn)線程每生產(chǎn)兩個(gè), 銷售線程才銷售1個(gè).
然后生產(chǎn)線程在生產(chǎn)完第11個(gè)產(chǎn)品, 嘗試生產(chǎn)產(chǎn)品12時(shí)拋異常被中斷了. 因?yàn)殇N售線程才銷售處第5個(gè).? 這時(shí)容器有6~11, 滿了, 爆倉..
接下來只有1個(gè)銷售線程執(zhí)行, 但是銷售完容器里面的產(chǎn)品后也拋異常了..? 因?yàn)閭}庫已經(jīng)沒有產(chǎn)品.
四, 線程的暫停 wait()
上面程序的銷售和執(zhí)行方法(enQueue 和 deQueue) 是同步的, 但是仍然會(huì)出錯(cuò).
1. 生產(chǎn)和銷售速度不一致.
2. 容器容量有限制.
所以必須對(duì)容器的入列和出列方法增加1個(gè)些處理.
其實(shí), 上面還是做了1寫處理的. 這個(gè)處理就是令它拋出異常..
如enQueue里面的.
if (this.isFull()){throw new RuntimeException("the warehouse is full!"); }意思就是容器滿了, 就拋異常中斷線程.
而現(xiàn)實(shí)中, 我們應(yīng)該這樣處理:?
如果容器滿了, 應(yīng)該把生產(chǎn)線程暫停.
如果容器空了, 應(yīng)該把銷售線程暫停.
4.1 sleep()方法并不適用
如果我們用sleep()方法來暫停一個(gè)線程是否可行呢? 例如
if (this.isFull()){Thread.sleep(10000) }sleep方法必須制定暫停的秒數(shù), 而在生產(chǎn)環(huán)境中, 我們通常無法判斷具體需要暫停多久的.
實(shí)際上, 在上面例子中, 我們需要生產(chǎn)線程暫停,直至容器不再為空.
那么容器什么時(shí)候不再為空呢, 取決于消費(fèi)線程.???
而生產(chǎn)環(huán)境中,消費(fèi)線程的消費(fèi)速度不是確定的.? 所以這里sleep()方法不適用于生產(chǎn)銷售問題.
4.2 wait()方法介紹
通常我們用wait()方法來暫停1個(gè)線程.? 首先看看jdk api 對(duì)wait()函數(shù)的介紹:
public final void wait()
??????????????? throws InterruptedException
在其他線程調(diào)用此對(duì)象的 notify() 方法或 notifyAll() 方法前,導(dǎo)致當(dāng)前線程等待
首先, wait()是基類Object的方法. 需要由1個(gè)實(shí)例化的對(duì)象來調(diào)用.
通常, 這個(gè)調(diào)用wait()方法的對(duì)象不應(yīng)該是線程對(duì)象, 而是線程鎖定的資源對(duì)應(yīng)的對(duì)象.
注意wait() 類似 sleep()會(huì)拋出異常, 必須手動(dòng)catch.
例如1個(gè)線程里的run()函數(shù).
public void run(){synchronized(a){xxxxxx();} }它為了與其他線程互斥, 鎖定了對(duì)象a.?
如果在xxxxx()方法中執(zhí)行了 a.wait() 則導(dǎo)致該線程暫停. 而且釋放該線程對(duì)資源a的鎖定
4.3 為生產(chǎn)銷售例子添加wait()方法.
實(shí)際上, 我們只需要修改容器類ProdQueue_1就ok了. 在enQueue() 和 deQueue()方法中都添加暫停的邏輯:
public synchronized void enQueue(Prod_1 p){while (this.isFull()){try{this.wait();}catch(Exception e){}}prod_q[pRear] = p;pRear = ((pRear + 1) + prod_q.length) % prod_q.length;} public synchronized Prod_1 deQueue(){while (this.isEmpty()){try{this.wait();}catch(Exception e){}} int p = pFront;pFront = ((pFront + 1) + prod_q.length) % prod_q.length; return prod_q[p];}上面我使用while來判斷 容器的狀態(tài)(滿or空), 而不是用if.
原因, 就是如果用while的話, 一旦被喚醒, 還會(huì)返回再檢查一次容器狀態(tài).? 而如果利用if一旦被喚醒,就直接執(zhí)行下面的代碼.
理論上, 用while是更加安全的.
這樣的話, 在生產(chǎn)線程中, 入列方法首先會(huì)判斷隊(duì)列容器是否已經(jīng)滿, 如果是滿的, 就會(huì)暫停線程, 并釋放鎖定的資源.
同樣, 在銷售線程中, 出列方法會(huì)首先判斷隊(duì)列容器是否為空, 如果是空的, 則暫停線程, 釋放資源.
注意, 這個(gè)例子中的sychronized 關(guān)鍵字是用來修飾方法名的. 也就是鎖定的資源是調(diào)用方法的對(duì)象本身, 也就是this了.
所以是執(zhí)行this.wait()來暫停線程.
4.4 輸出結(jié)果
添加了wait()方法后, 輸出結(jié)果如下:
Producer: made the product 0 Seller: sold the product 0 Producer: made the product 1 Producer: made the product 2 Seller: sold the product 1 Producer: made the product 3 Producer: made the product 4 Seller: sold the product 2 Producer: made the product 5 Producer: made the product 6 Seller: sold the product 3 Producer: made the product 7 Producer: made the product 8 Seller: sold the product 4 Producer: made the product 9 Producer: made the product 10 Seller: sold the product 5 Producer: made the product 11 Seller: sold the product 6 Seller: sold the product 7 Seller: sold the product 8 Seller: sold the product 9 Seller: sold the product 10 Seller: sold the product 11
可以看出, 需要沒有拋出異常, 但是實(shí)際效果仍然跟上次相似.
當(dāng)生產(chǎn)線程入列第11個(gè)產(chǎn)品后, 嘗試入列第12個(gè)時(shí), 這時(shí)容器滿了, 生產(chǎn)線程被暫停.
這時(shí)只剩下銷售線程在執(zhí)行, 最終銷售完第11個(gè)產(chǎn)品時(shí), 容器空了, 銷售線程也被暫停.
這時(shí)程序只剩下主線程了, 相當(dāng)于死機(jī)狀態(tài).
原因是兩個(gè)業(yè)務(wù)線程都暫停了,處于等待狀態(tài).
這時(shí)就需要1個(gè)喚醒機(jī)制了.
五, 線程的喚醒 notify()
我們首先來看看jdk api 對(duì) notfiy() 方法的介紹.
public final void notify()
喚醒在此對(duì)象監(jiān)視器上等待的單個(gè)線程。如果所有線程都在此對(duì)象上等待,則會(huì)選擇喚醒其中一個(gè)線程。選擇是任意性的,并在對(duì)實(shí)現(xiàn)做出決定時(shí)發(fā)生。線程通過調(diào)用其中一個(gè) wait 方法,在對(duì)象的監(jiān)視器上等待。
我在以前的博文提過了, jdk api中文的翻譯水平不是很好.
但是起碼要弄明白,? notify()是基類Object的一個(gè)非靜態(tài)方法. 一般是由調(diào)用wait()的對(duì)象(被鎖定的資源)來調(diào)用.?
意思就是假如 對(duì)象A調(diào)用wait() 暫停了線程(A是被鎖定的資源對(duì)象), 則必須執(zhí)行A.notfiy()來喚醒.
本屌表達(dá)能力也很有限, 還是結(jié)合上面例子說明:
但是首先要明白如下幾個(gè)概念:
5.1 假如A線程被暫停, 那么誰來喚醒A
一個(gè)線程被暫停后就不能執(zhí)行, 所以線程是不能喚醒自己的.
只能由其他線程喚醒.
在這個(gè)例子中個(gè), 暫停中的生產(chǎn)線程只能被銷售線程喚醒.? 同樣地, 暫停中的銷售線程只能被生產(chǎn)線程喚醒.
當(dāng)然, 在主線程也可以喚醒它們, 但是不符合業(yè)務(wù)邏輯.
5.2 什么時(shí)候喚醒.
這也是個(gè)問題, 在這個(gè)問題中, 我們可以這樣設(shè)置:
當(dāng)生產(chǎn)線程成功生產(chǎn)1個(gè)新產(chǎn)品入列, 這時(shí)容器就肯定不為空了, 我們就可以讓生產(chǎn)線程嘗試喚醒銷售線程.
同樣, 當(dāng)銷售線程成功出列1個(gè)產(chǎn)品時(shí), 這時(shí)容器就肯定不是滿的, 我們就可以讓銷售線程嘗試喚醒生產(chǎn)線程.
5.3 notify()到底喚醒了哪個(gè)線程.
在上面之所以紅色高亮了"嘗試"這個(gè)詞, 是因?yàn)閚otify()方法無法喚醒1個(gè)指定的線程.
假如有兩個(gè)線程執(zhí)行了this.wait()而暫停, 那么在第3個(gè)線程中執(zhí)行this.notfiy()會(huì)隨機(jī)喚醒其中1個(gè).
當(dāng)然, 這個(gè)例子中我們不允許兩個(gè)線程都被暫停, 所以執(zhí)行this.notify()就是喚醒對(duì)方線程了.
而某一時(shí)間, 沒有任何線程因?yàn)閠his.wait()而暫停, 那么執(zhí)行this.notify()則不起任何作用, 但是不會(huì)拋出任何異常和報(bào)錯(cuò)!
也就說, 當(dāng)生產(chǎn)線程執(zhí)行this.notify()時(shí)無需事先判斷銷售線程的狀態(tài).? 反之亦然.
5.4 修改后的出列和入列方法代碼.
既然邏輯理順了, 那么代碼就很簡單:
public synchronized void enQueue(Prod_1 p){while (this.isFull()){try{this.wait();}catch(Exception e){}}prod_q[pRear] = p;pRear = ((pRear + 1) + prod_q.length) % prod_q.length;this.notify();} public synchronized Prod_1 deQueue(){while (this.isEmpty()){try{this.wait();}catch(Exception e){}} int p = pFront;pFront = ((pFront + 1) + prod_q.length) % prod_q.length; this.notify();return prod_q[p];}邏輯很簡單, 無非就是在入列和出列的最后添加this.notify(), 每次成功生產(chǎn)or銷售1個(gè)產(chǎn)品, 都嘗試去喚醒對(duì)方線程.
5.6 輸出結(jié)果
經(jīng)過這次修改后, 結(jié)果如下:
hello ant, it's the my meeting with ant! Producer: made the product 0 Seller: sold the product 0 Producer: made the product 1 Producer: made the product 2 Seller: sold the product 1 Producer: made the product 3 Producer: made the product 4 Seller: sold the product 2 Producer: made the product 5 Producer: made the product 6 Seller: sold the product 3 Producer: made the product 7 Producer: made the product 8 Seller: sold the product 4 Producer: made the product 9 Producer: made the product 10 Seller: sold the product 5 Producer: made the product 11 Seller: sold the product 6 Producer: made the product 12 Seller: sold the product 7 Producer: made the product 13 Seller: sold the product 8 Producer: made the product 14 Seller: sold the product 9 Producer: made the product 15 Seller: sold the product 10 Producer: made the product 16 Seller: sold the product 11 Producer: made the product 17 Seller: sold the product 12 Producer: made the product 18 Seller: sold the product 13 Producer: made the product 19 Seller: sold the product 14 Seller: sold the product 15 Seller: sold the product 16 Seller: sold the product 17 Seller: sold the product 18 Seller: sold the product 19可以看出由于速度的不同, 在11個(gè)產(chǎn)品前, 生產(chǎn)線程生產(chǎn)兩個(gè), 銷售線程才銷售出1個(gè).
但是在生產(chǎn)11個(gè)產(chǎn)品時(shí), 容器滿了, 生產(chǎn)線程被暫停.
然后銷售線程銷售出第6個(gè)產(chǎn)品時(shí), 喚醒了生產(chǎn)線程.
生產(chǎn)線程生產(chǎn)出第12個(gè)產(chǎn)品, 這時(shí)又滿了, 再次暫停...
所以后面就是生產(chǎn)和銷售線程交替1個(gè)1個(gè)地生產(chǎn)和銷售....
從這個(gè)例子中看出在后面的處理似乎體現(xiàn)不出生產(chǎn)線程的速度優(yōu)勢(shì),? 但是在實(shí)際項(xiàng)目中, 生產(chǎn)和銷售的速度并不是固定的.
這個(gè)方法其實(shí)是相對(duì)合理的方法, 解決了本文開始的那個(gè)問題.
六, 喚醒所有線程 notifyAll()
notifyAll()也不難理解.
假如上面的題目修改一下, 有兩條生產(chǎn)線程和兩條銷售線程 共4個(gè)線程共享1個(gè)隊(duì)列容器.
那么同一時(shí)間可能有多條被暫停.
但是notify()方法只會(huì)喚醒隨機(jī)的一條線程.
所以有時(shí)就有必要用notifyAll()來喚醒所有暫停中的線程了!
七, suspend() 和 resume()
這個(gè)兩個(gè)方法是類Thread 的非靜態(tài)方法.
用這個(gè)兩個(gè)方法也可以實(shí)現(xiàn)線程的掛起和恢復(fù), 但是suspend()掛起時(shí)并不釋放被鎖定的資源, 容易造成死鎖,? JDK API中明確表明不建議使用這個(gè)兩個(gè)方法!
總結(jié)
以上是生活随笔為你收集整理的Java线程的挂起与恢复 wait(), notify()方法介绍的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 里的字符串处理类StringB
- 下一篇: Java里的容器 Collection