引子: java編程中有時候會要求線程安全(注:多個線程同時訪問同一代碼的時候,不會產生不同的結果。編寫線程安全的代碼需要線程同步),這時候就需要進行多線程編程。從而用到線程間通信的技術。那么在java里面,線程間通信是怎么實現的?這篇文章將通過一個案例詳細分析。
文章關鍵詞: Object,wait,notify,notifyAll,鎖,同步(synchronized).
詳解一個經典的生產者消費者模型,其中用到了 wait和notifyAll方法。
源碼如下:
1 2
3 import java.util.LinkedList; 4 import java.util.Queue; 5
6 public class MainTest { 7 public static void main(String[] args) { 8 test(); 9 } 10
11 private static final long waitTime = 3000; 12
13 private static void test() { 14 Queue<Integer> queue = new LinkedList<>();// 隊列對象,它就是所謂的“鎖”
15 int maxsize = 2;// 隊列中的最大元素個數限制 16
17 // 下面4個線程,一瞬間只能有一個線程獲得該對象的鎖,而進入同步代碼塊
18 Producer producer = new Producer(queue, maxsize, "Producer"); 19 Consumer consumer1 = new Consumer(queue, maxsize, "Consumer1"); 20 Consumer consumer2 = new Consumer(queue, maxsize, "Consumer2"); 21 Consumer consumer3 = new Consumer(queue, maxsize, "Consumer3"); 22
23 // 其實隨便先啟動哪個都無所謂,因為只有一個鎖,每一次只會有一個線程能持有這個鎖,來操作queue
24 producer.start(); 25 consumer2.start(); 26 consumer1.start(); 27 consumer3.start(); 28 } 29
30 /**
31 * 生產者線程 32 */
33 public static class Producer extends Thread { 34 Queue<Integer> queue;// queue,對象鎖
35 int maxsize;// 貌似是隊列的最大產量
36
37 Producer(Queue<Integer> queue, int maxsize, String name) { 38 this.queue = queue; 39 this.maxsize = maxsize; 40 this.setName(name); 41 } 42
43 @Override 44 public void run() { 45 while (true) {// 無限循環,不停生產元素,直到達到上限,只要達到上限,那就wait等待。
46 synchronized (queue) {// 同步代碼塊,只有持有queue這個鎖的對象才能訪問這個代碼塊
47 try { 48 Thread.sleep(waitTime); 49 // sleep和wait的區別,sleep會讓當前執行的線程阻塞一段時間,但是不會釋放鎖, 50 // 但是wait,會阻塞,並且會釋放鎖
51 } catch (Exception e) { 52 } 53
54 System.out.println(this.getName() + "獲得隊列的鎖");// 只有你獲得了queue對象的鎖,你才能執行到這里 55 // 條件的判斷一定要使用while而不是if
56 while (queue.size() == maxsize) {// 判斷生產有沒有達到上限,如果達到了上限,就讓當前線程等待
57 System.out.println("隊列已滿,生產者" + this.getName() + "等待"); 58 try { 59 queue.wait();// 讓當前線程等待,直到其他線程調用notifyAll
60 } catch (Exception e) { 61 } 62 } 63
64 // 下面寫的就是生產過程
65 int num = (int) (Math.random() * 100); 66 queue.offer(num);// 將一個int數字插入到隊列中
67
68 System.out.println(this.getName() + "生產一個元素:" + num); 69 // 喚醒其他線程,在這里案例中是 "等待中"的消費者線程
70 queue.notifyAll();// (注:notifyAll的作用是 71 // 喚醒所有持有queue對象鎖的正在等待的線程)
72
73 System.out.println(this.getName() + "退出一次生產過程!"); 74 } 75 } 76 } 77 } 78
79 public static class Consumer extends Thread { 80 Queue<Integer> queue; 81 int maxsize; 82
83 Consumer(Queue<Integer> queue, int maxsize, String name) { 84 this.queue = queue; 85 this.maxsize = maxsize; 86 this.setName(name); 87 } 88
89 @Override 90 public void run() { 91 while (true) { 92 synchronized (queue) {// 要想進入下面的代碼,就必須先獲得鎖。
93 try { 94 Thread.sleep(waitTime);// sleep,讓當前線程阻塞指定時長,但是並不會釋放queue鎖
95 } catch (Exception e) { 96 } 97
98 System.out.println(this.getName() + "獲得隊列的鎖");// 拿到了鎖,才能執行到這里 99 // 條件的判斷一定要使用while而不是if,
100 while (queue.isEmpty()) {// while判斷隊列是否為空,如果為空,當前消費者線程就必須wait,等生產者先生產元素 101 // 這里,消費者有多個(因為有多個consumer線程),每一個消費者如果發現了隊列空了,就會wait。
102 System.out.println("隊列為空,消費者" + this.getName() + "等待"); 103 try { 104 queue.wait(); 105 } catch (Exception e) { 106 } 107 } 108
109 // 如果隊列不是空,那么就彈出一個元素
110 int num = queue.poll(); 111 System.out.println(this.getName() + "消費一個元素:" + num); 112 queue.notifyAll();// 然后再喚醒所有線程,喚醒不會釋放自己的鎖
113
114 System.out.println(this.getName() + "退出一次消費過程!"); 115 } 116 } 117 } 118 } 119 }
案例解析:
1)此案例模擬的是,生產者線程 生產元素並且插入到Queue中,Queue有一個存儲個數的限制。消費者線程,從Queue中拿出元素。兩個線程都是無限循環執行的。
2)在生產者線程的生產過程(隨機產生一個int然后插入到queue中)執行之前,首先檢查Queue的存儲個數有沒有到達上限,如果到達了,那就不能生產,代碼中調用了queue.wait();來使生產者線程進入等待狀態並且釋放鎖。如果沒超過,那就反復執行,直到到達上限。
3)消費者線程在執行消費過程(從queue中彈出一個元素)執行之前,首先要檢查queue是不是空,如果是空,那就不能消費,調用queue.wait()讓消費線程進入等待狀態並且釋放鎖。
4)在生產過程 或 消費過程執行完畢之后,都會有queue.notifyAll();來喚醒等待鎖的所有線程。
5)生產者中,判定queue的元素個數是不是到達上限。以及 消費者中,判定queue是不是空,這種判定queue.wait()的條件 所使用的關鍵字,並不是if,而是while.
因為在執行了wait之后,該線程的執行,會暫時停留在這個while循環中,等待被喚醒,一旦被喚醒,while循環會繼續執行,從而會再次判斷條件是否滿足。
6)代碼中能找到Thread.sleep(long);方法,它的作用,是當當前線程阻塞指定時間,但是它並不會釋放鎖。而wait除了阻塞之外,還會釋放鎖。
案例執行的結果打印:
Producer獲得隊列的鎖
Producer生產一個元素:86
Producer退出一次生產過程!
Producer獲得隊列的鎖
Producer生產一個元素:31
Producer退出一次生產過程!
Producer獲得隊列的鎖
隊列已滿,生產者Producer等待
Consumer2獲得隊列的鎖
Consumer2消費一個元素:86
Consumer2退出一次消費過程!
Consumer3獲得隊列的鎖
Consumer3消費一個元素:31
Consumer3退出一次消費過程!
Consumer1獲得隊列的鎖
隊列為空,消費者Consumer1等待
Consumer3獲得隊列的鎖
隊列為空,消費者Consumer3等待
Consumer2獲得隊列的鎖
隊列為空,消費者Consumer2等待
Producer生產一個元素:29
Producer退出一次生產過程!
Producer獲得隊列的鎖
Producer生產一個元素:82
Producer退出一次生產過程!
Producer獲得隊列的鎖
隊列已滿,生產者Producer等待
Consumer2消費一個元素:29
Consumer2退出一次消費過程!
結果分析(請對照日志來看,大神請繞道,下面的描述比較啰嗦):
由於首先啟動的是生產者線程(Producer),所以producer先獲得了鎖,進行了兩次生產。再次嘗試生產的時候發現queue滿了,於是,生產者進入等待。
之后,consumer2的得到了鎖,於是進行消費,消費執行了一次,鎖被consumer3奪走,consumer3執行了一次消費。
之后,consumer1得到了鎖,就當它准備開始消費的時候,發現queue空了,不能消費了,於是代碼調用queue.wait().來讓consumer1進入等待。
之后,consumer3和consumer2相繼得到鎖,但是他們都發現,queue空了,也不能消費,於是同樣調用queue.wait()來讓consumer3和consumer1進入等待。
再然后,生產者得到了鎖(這里可能很奇怪,生產者不是在等待么?它什么時候被喚醒的,查看Consumer的代碼,能發現,在每一次成功消費之后,都會有queue.notifyAll(),也就是說,在之前cunsumer2消費之后,生產者就已經被喚醒了,只是他沒有得到鎖,所以就沒有執行生產過程)。
生產者得到鎖之后,繼續while循環,發現queue並沒有填滿,於是進入生產過程。之后···就是無限循環了。
這種模型在線程安全比較高的場景中,會被經常用到,比如買票系統,同一張票不能被賣兩次。所以,這張票,在同一時間只能被一個線程訪問。
-------------------
案例解析完畢,但是針對java多線程,也許有人會有其他疑問,下面列舉幾個比較重要的問題加以說明:
問:在java中,wait,notify以及notifyAll是用來做線程之間的通信的,但是為什么這3個方法不是在Thread類里面,而是在Object類里面?
答:
這3個方法雖然是用於線程間的通信,但是他們並不是直接就在Thread類里面,而是在Object類。
這是 因為 調用一個Object的wait,notify,notifyAll 必須保證該段代碼對於該Object是同步的, 否則就可能會報異常IllegalMonitorStateException(具體可以進入Object類的源碼搜索此異常,注釋中有詳細說明),通常的寫法如下,
synchronized(obj){//在執行wait,notify,notifyAll時,必須保證這段代碼持有obj對象的鎖。
obj.wait();
...
obj.notify();
...
obj.notifyAll();
}
如果多個線程都寫了上面的代碼,那么同一時間,只會有一個線程能獲取obj對象鎖。
所以說,這3個方法在Object類里,而不是在Thread類里,其實是java框架的設定,通過Object鎖來完成線程間的通信。
問:wait,notify,notifyAll的作用分別是什么?
答:
wait-讓當前線程進入等待狀態,並且釋放鎖;
notify -喚醒任意一個正在等待鎖的線程,並且讓它得到鎖。
notifyAll,喚醒所有等待對象鎖的線程,如果有多個線程都被喚醒,那么鎖將會被他們爭奪,同一時間只會有一個線程得到鎖。
問:notify,notifyAll有啥區別?
答:
notify,讓任意一個等待對象鎖的線程得到鎖,並且喚醒他。
notifyAll,喚醒所有等到對象鎖的線程,如果有多個被喚醒的線程,鎖將會被爭奪,爭奪到鎖的線程就可以執行.
===================就寫到這里了。上面的是基礎知識,在復雜場景中可能會被復雜化千萬倍,但是萬變不離其宗,了解了原理,就能應對大部分場景了。
