大家好,上篇文章為大家介紹了線程間通信和協作的一些基本方式,那這篇文章就來介紹一下經典的wait-notify機制吧。 什麼是wait-notify機制? 想象一下有兩個線程A、B,如果業務場景中需要這兩個線程交替執行任務(比如A執行完一次任務後換B執行,B執行完後再換A執行這樣重覆交替),之前的基 ...
大家好,上篇文章為大家介紹了線程間通信和協作的一些基本方式,那這篇文章就來介紹一下經典的wait-notify機制吧。
什麼是wait-notify機制?
想象一下有兩個線程A、B,如果業務場景中需要這兩個線程交替執行任務(比如A執行完一次任務後換B執行,B執行完後再換A執行這樣重覆交替),之前的基本通信方式只能讓線程暫停一段指定時間,Join方法也無法做到這種交替執行的要求,那怎麼辦呢?
別急,針對這種場景java同樣為我們提供了一種經典的線程通信方式——wait-notify機制,這裡涉及到下麵的三個方法(關於鎖的知識後文會詳細講):
wait方法:當前線程在調用wait方法會先讓出當前線程持有的對象鎖以便讓其他線程能夠獲取,然後當前線程會停止執行併進入WAITING狀態。直到接收到喚醒或中斷信號後,當前線程才會繼續嘗試獲取對象鎖。如果此時獲取對象鎖成功,就能繼續執行任務。
notify方法:當前線程的任務即將執行完畢併發出喚醒信號,此時只有接收到喚醒信號的線程才會嘗試獲取對象鎖。當然此時可能獲取對象鎖會失敗,因為notify方法不會即時釋放鎖,而是需要等到線程執行完畢後才會真正釋放鎖。
notifyAll方法:和notify方法作用相似,唯一不同的就是該方法會對當前所有在等待這個對象鎖的線程發出喚醒信號。至於最終是哪個線程搶到了對象鎖,就要看哪個線程比較“幸運”啦。
關於這幾個方法,還有以下兩點需要關註:
1.wait、notify、notifyAll這三個方法都是Object類中定義的,而Object類是所有類的父類,所以在java中的所有對象都會繼承這三個方法。
2.這三個方法必須在同步塊中被調用(之後會介紹同步塊),如果在同步塊之外調用這三個方法,java會拋出java.lang.IllegalMonitorStateException這個異常。
基於wait-notify機制的單生產者-單消費者模型
上面已經介紹了wait-notify機制用到的方法以及需要註意的點,實際上針對這個機制,有一個非常著名、非常經典的模型——生產者消費者模型。
什麼是生產者-消費者模型呢?簡單來說就是這麼個場景:有兩種線程分別是生產者線程和消費者線程,還有一個固定大小的資源隊列。
生產者的任務是根據原料生產出產品,並將生產好的產品往隊列里扔;消費者的任務呢就是從隊列裡面拿已經生產好的產品去進行包裝。
我們可以看到在這個場景中,因為隊列可容納的資源是有限的,所以當隊列滿時,生產者就沒辦法繼續往隊列里放產品,此時生產者就需要等待消費者從隊列里拿走產品後,才能繼續往隊列里放產品;
而消費者也是一樣,當隊列為空時,消費者就無法從隊列里拿到產品,此時就需要等待生產者成功生產出產品並往隊列里扔,才能繼續從隊列里拿產品。
這個場景是wait-notify機制最適合發揮作用的場景,下麵是一個單生產者-單消費者的模擬代碼:
1 /** 2 * 基於wait-notify機制的單生產者-消費者模型 3 */ 4 public class ProducerAndConsumer { 5 6 public static void main(String[] args) { 7 Resource resource = new Resource(); 8 //生產者線程 9 ProducerThread p1 = new ProducerThread(resource); 10 //消費者線程 11 ConsumerThread c1 = new ConsumerThread(resource); 12 13 p1.start(); 14 c1.start(); 15 16 } 17 } 18 19 20 /** 21 * 公共資源類 22 * @author 23 * 24 */ 25 class Resource{//重要 26 //當前資源數量 27 private int num = 0; 28 //資源池中允許存放的資源數目 29 private int size = 10; 30 31 /** 32 * 從資源池中取走資源 33 */ 34 public synchronized void remove(){ 35 if(num > 0){ 36 num--; 37 System.out.println("消費者" + Thread.currentThread().getName() + 38 "消耗一件資源," + "當前線程池有" + num + "個"); 39 notifyAll();//通知生產者生產資源 40 }else{ 41 try { 42 //如果沒有資源,則消費者進入等待狀態 43 wait(); 44 System.out.println("消費者" + Thread.currentThread().getName() + "線程進入等待狀態"); 45 } catch (InterruptedException e) { 46 e.printStackTrace(); 47 } 48 } 49 } 50 /** 51 * 向資源池中添加資源 52 */ 53 public synchronized void add(){ 54 if(num < size){ 55 num++; 56 System.out.println("生產者" + Thread.currentThread().getName() + "生產一件資源,當前資源池有" 57 + num + "個"); 58 //通知等待的消費者 59 notifyAll(); 60 }else{ 61 //如果當前資源池中有10件資源 62 try{ 63 wait();//生產者進入等待狀態,並釋放鎖 64 System.out.println(Thread.currentThread().getName()+"線程進入等待"); 65 }catch(InterruptedException e){ 66 e.printStackTrace(); 67 } 68 } 69 } 70 } 71 72 73 /** 74 * 消費者線程 75 */ 76 class ConsumerThread extends Thread{ 77 private Resource resource; 78 public ConsumerThread(Resource resource){ 79 this.resource = resource; 80 } 81 @Override 82 public void run() { 83 while(true){ 84 try { 85 Thread.sleep(1000); 86 } catch (InterruptedException e) { 87 e.printStackTrace(); 88 } 89 resource.remove(); 90 } 91 } 92 } 93 94 95 /** 96 * 生產者線程 97 */ 98 class ProducerThread extends Thread{ 99 private Resource resource; 100 public ProducerThread(Resource resource){ 101 this.resource = resource; 102 } 103 @Override 104 public void run() { 105 //不斷地生產資源 106 while(true){ 107 try { 108 Thread.sleep(1000); 109 } catch (InterruptedException e) { 110 e.printStackTrace(); 111 } 112 resource.add(); 113 } 114 } 115 116 }View Code
童鞋們可以運行代碼試試,這裡資源池最大允許放10個產品。
這裡留一個問題給大家思考,如果我這裡的add和remove方法不加synchronized修飾,就會拋出java.lang.IllegalMonitorStateException異常,那麼是什麼原因導致java必須要這麼做呢?我會在介紹synchronized關鍵字的時候公佈答案。
好了,wait-notify機制到這裡就介紹完畢,希望大家能夠理解。下篇文章會為大家講解一下volatile這個關鍵字的用法。