Java线程的挂起与恢复 wait(), notify()方法介绍
一, 什么是線程的掛起與恢復
從字面理解也很簡單.
所謂線程掛起就是指暫停線程的執行(阻塞狀態).
而恢復時就是讓暫停的線程得以繼續執行.(返回就緒狀態)
二, 為何需要掛起和恢復線程.
我們來看1個經典的例子(生產消費):
1個倉庫最多容納6個產品, 制造者現在需要制造超過20件產品存入倉庫, 銷售者要從倉庫取出這20件產品來消費.
制造和消費的速度很可能是不一樣的, 編程實現兩者的同步.
1. 把倉庫設為1個容器, 容量為6
2. 把生產 和 消費設為兩線程.
3. 無論生產還是消費, 每次的個數都是1.
4. 生產線程往倉庫添加20個產品, 消費線程從倉庫取20個產品.
5. 倉庫滿時, 生產線程必須暫停, 倉庫空時, 消費必須暫停.
可見, 為了滿足第5個條件, 線程必須有暫停和恢復執行的功能. 這就是需要掛起和恢復線程的原因.
其實這個問題還有1個隱藏條件:
因為有2個線程同時訪問修改同1個數據(容器), 所以生產和消費線程的關鍵代碼必須是互斥的.
亦即系講, 當生產線程訪問和修改容器時, 恢復線程就必須阻塞, 否則數據會出錯.
三, 生產消費問題的簡單代碼(不考慮掛起恢復)
我們一步一步利用java代碼來實現這個問題.
3.1 產品
我們只需要定義1個類來描述產品.
本例中的產品只有1個屬性id (int 類型)用于區分.
代碼:
class Prod_1{private int id;public int getId(){return this.id;}public Prod_1(int id){this.id = id;} }3.2 倉庫(容器)
上面說過了, 倉庫的數據模型,實際上就是1個容器.
編程中常用的容器無非是棧和隊列. (每次進出的個數都只能是1)
而在倉庫管理中, 一般會優先把生產日期早的產品出庫(先進先出), 所以這里就用隊列來實現了
例子中的隊列是1個靜態(數組)隊列.
靜態隊列的代碼實現其實并不容易理解. 如果有興趣的話可以看看本人關于靜態隊列的介紹: http://blog.csdn.net/nvd11/article/details/8816699
但在這里, 只需要明白兩個方法作用就ok了
1. public synchronized void enqueue(object ob)
作用是把ob放入隊列中, 也就是入庫.
2. public synchronized ob deQueue()
把隊列中最早放入的元素(返回值)拿出來, 也就是出庫.
注意上面兩個方法是同步的, 也就是互斥的.
代碼:
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 生產線程
生產線程無非就是不斷調用容器類的入列函數(enQueue),把產品不斷放入容器中.
當然要接受上面容器類作為1個成員.
而且因為是線程類, 必須實現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());}} }上面就是生產者的類, 構造方法中有兩個參數, 分別對應其兩個成員:
1個就是容器的對象,? 另1個就是要生產的產品數量.
注意, 我在run()方法的循環中加了1個sleep()方法, 代表每生產1個產品停頓1秒(設置生產速度)
3.4 銷售線程
銷售線程的業務就是不斷地從容器中取出產品. 就是執行容器對象的deQueue()方法了.
具體實現方法跟生產線程是類似的. 代碼如下:
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());}}}在銷售線程中, 每個循環利用sleep()方法停頓2秒, 也就設置了銷售速度是比生產速度慢一倍的.
3.5 啟動類
國際慣例, 在1個啟動類的靜態方法中,調用上面寫的業務類.
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個容器對象.
然后利用這個容器對象構造1個生產線程對象和1個銷售線程對象.
最后啟動這個兩個線程.
3.6 執行結果
執行結果如下:
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)可見到 結果中:
1開始, 生產線程和銷售線程是正常執行的, 因為速度的不同, 大概生產線程每生產兩個, 銷售線程才銷售1個.
然后生產線程在生產完第11個產品, 嘗試生產產品12時拋異常被中斷了. 因為銷售線程才銷售處第5個.? 這時容器有6~11, 滿了, 爆倉..
接下來只有1個銷售線程執行, 但是銷售完容器里面的產品后也拋異常了..? 因為倉庫已經沒有產品.
四, 線程的暫停 wait()
上面程序的銷售和執行方法(enQueue 和 deQueue) 是同步的, 但是仍然會出錯.
1. 生產和銷售速度不一致.
2. 容器容量有限制.
所以必須對容器的入列和出列方法增加1個些處理.
其實, 上面還是做了1寫處理的. 這個處理就是令它拋出異常..
如enQueue里面的.
if (this.isFull()){throw new RuntimeException("the warehouse is full!"); }意思就是容器滿了, 就拋異常中斷線程.
而現實中, 我們應該這樣處理:?
如果容器滿了, 應該把生產線程暫停.
如果容器空了, 應該把銷售線程暫停.
4.1 sleep()方法并不適用
如果我們用sleep()方法來暫停一個線程是否可行呢? 例如
if (this.isFull()){Thread.sleep(10000) }sleep方法必須制定暫停的秒數, 而在生產環境中, 我們通常無法判斷具體需要暫停多久的.
實際上, 在上面例子中, 我們需要生產線程暫停,直至容器不再為空.
那么容器什么時候不再為空呢, 取決于消費線程.???
而生產環境中,消費線程的消費速度不是確定的.? 所以這里sleep()方法不適用于生產銷售問題.
4.2 wait()方法介紹
通常我們用wait()方法來暫停1個線程.? 首先看看jdk api 對wait()函數的介紹:
public final void wait()
??????????????? throws InterruptedException
在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待
首先, wait()是基類Object的方法. 需要由1個實例化的對象來調用.
通常, 這個調用wait()方法的對象不應該是線程對象, 而是線程鎖定的資源對應的對象.
注意wait() 類似 sleep()會拋出異常, 必須手動catch.
例如1個線程里的run()函數.
public void run(){synchronized(a){xxxxxx();} }它為了與其他線程互斥, 鎖定了對象a.?
如果在xxxxx()方法中執行了 a.wait() 則導致該線程暫停. 而且釋放該線程對資源a的鎖定
4.3 為生產銷售例子添加wait()方法.
實際上, 我們只需要修改容器類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來判斷 容器的狀態(滿or空), 而不是用if.
原因, 就是如果用while的話, 一旦被喚醒, 還會返回再檢查一次容器狀態.? 而如果利用if一旦被喚醒,就直接執行下面的代碼.
理論上, 用while是更加安全的.
這樣的話, 在生產線程中, 入列方法首先會判斷隊列容器是否已經滿, 如果是滿的, 就會暫停線程, 并釋放鎖定的資源.
同樣, 在銷售線程中, 出列方法會首先判斷隊列容器是否為空, 如果是空的, 則暫停線程, 釋放資源.
注意, 這個例子中的sychronized 關鍵字是用來修飾方法名的. 也就是鎖定的資源是調用方法的對象本身, 也就是this了.
所以是執行this.wait()來暫停線程.
4.4 輸出結果
添加了wait()方法后, 輸出結果如下:
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
可以看出, 需要沒有拋出異常, 但是實際效果仍然跟上次相似.
當生產線程入列第11個產品后, 嘗試入列第12個時, 這時容器滿了, 生產線程被暫停.
這時只剩下銷售線程在執行, 最終銷售完第11個產品時, 容器空了, 銷售線程也被暫停.
這時程序只剩下主線程了, 相當于死機狀態.
原因是兩個業務線程都暫停了,處于等待狀態.
這時就需要1個喚醒機制了.
五, 線程的喚醒 notify()
我們首先來看看jdk api 對 notfiy() 方法的介紹.
public final void notify()
喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,并在對實現做出決定時發生。線程通過調用其中一個 wait 方法,在對象的監視器上等待。
我在以前的博文提過了, jdk api中文的翻譯水平不是很好.
但是起碼要弄明白,? notify()是基類Object的一個非靜態方法. 一般是由調用wait()的對象(被鎖定的資源)來調用.?
意思就是假如 對象A調用wait() 暫停了線程(A是被鎖定的資源對象), 則必須執行A.notfiy()來喚醒.
本屌表達能力也很有限, 還是結合上面例子說明:
但是首先要明白如下幾個概念:
5.1 假如A線程被暫停, 那么誰來喚醒A
一個線程被暫停后就不能執行, 所以線程是不能喚醒自己的.
只能由其他線程喚醒.
在這個例子中個, 暫停中的生產線程只能被銷售線程喚醒.? 同樣地, 暫停中的銷售線程只能被生產線程喚醒.
當然, 在主線程也可以喚醒它們, 但是不符合業務邏輯.
5.2 什么時候喚醒.
這也是個問題, 在這個問題中, 我們可以這樣設置:
當生產線程成功生產1個新產品入列, 這時容器就肯定不為空了, 我們就可以讓生產線程嘗試喚醒銷售線程.
同樣, 當銷售線程成功出列1個產品時, 這時容器就肯定不是滿的, 我們就可以讓銷售線程嘗試喚醒生產線程.
5.3 notify()到底喚醒了哪個線程.
在上面之所以紅色高亮了"嘗試"這個詞, 是因為notify()方法無法喚醒1個指定的線程.
假如有兩個線程執行了this.wait()而暫停, 那么在第3個線程中執行this.notfiy()會隨機喚醒其中1個.
當然, 這個例子中我們不允許兩個線程都被暫停, 所以執行this.notify()就是喚醒對方線程了.
而某一時間, 沒有任何線程因為this.wait()而暫停, 那么執行this.notify()則不起任何作用, 但是不會拋出任何異常和報錯!
也就說, 當生產線程執行this.notify()時無需事先判斷銷售線程的狀態.? 反之亦然.
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(), 每次成功生產or銷售1個產品, 都嘗試去喚醒對方線程.
5.6 輸出結果
經過這次修改后, 結果如下:
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個產品前, 生產線程生產兩個, 銷售線程才銷售出1個.
但是在生產11個產品時, 容器滿了, 生產線程被暫停.
然后銷售線程銷售出第6個產品時, 喚醒了生產線程.
生產線程生產出第12個產品, 這時又滿了, 再次暫停...
所以后面就是生產和銷售線程交替1個1個地生產和銷售....
從這個例子中看出在后面的處理似乎體現不出生產線程的速度優勢,? 但是在實際項目中, 生產和銷售的速度并不是固定的.
這個方法其實是相對合理的方法, 解決了本文開始的那個問題.
六, 喚醒所有線程 notifyAll()
notifyAll()也不難理解.
假如上面的題目修改一下, 有兩條生產線程和兩條銷售線程 共4個線程共享1個隊列容器.
那么同一時間可能有多條被暫停.
但是notify()方法只會喚醒隨機的一條線程.
所以有時就有必要用notifyAll()來喚醒所有暫停中的線程了!
七, suspend() 和 resume()
這個兩個方法是類Thread 的非靜態方法.
用這個兩個方法也可以實現線程的掛起和恢復, 但是suspend()掛起時并不釋放被鎖定的資源, 容易造成死鎖,? JDK API中明確表明不建議使用這個兩個方法!
總結
以上是生活随笔為你收集整理的Java线程的挂起与恢复 wait(), notify()方法介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 里的字符串处理类StringB
- 下一篇: Java里的容器 Collection