1. 程式人生 > >基於線程實現的生產者消費者模型(Object.wait(),Object.notify()方法)

基於線程實現的生產者消費者模型(Object.wait(),Object.notify()方法)

生產者 nbsp 來看 spa res strong read 在哪裏 那一刻

需求背景

利用線程來模擬生產者和消費者模型

系統建模

這個系統涉及到三個角色,生產者,消費者,任務隊列,三個角色之間的關系非常簡單,生產者和消費者擁有一個任務隊列的引用,生產者負責往隊列中放置對象(id),消費者負責從隊列中獲取對象(id),其關聯關系如下

技術分享

方案1

因為是多線程操作,所以對任務的存取都要使用線程同步加鎖機制,看一下我們的TaskQueue類,兩個主方法都加了synchronized修飾,這就意味著,一個時間點只可能有一個線程對這個方法進行操作

TaskQueue類代碼

[java] view plain copy print?
  1. package com.crazycoder2010.thread;
  2. public class TaskQueue {
  3. private int id;
  4. public synchronized void put(int id){
  5. this.id = id;
  6. System.out.println("Put:"+id);
  7. }
  8. public synchronized int get(){
  9. System.out.println("Got:"+this.id);
  10. return this.id;
  11. }
  12. }

再來看一下生產者,這個主要不停的往TaskQueue中添加新對象,這裏我們搞了個死循環,運行時不斷的對當前數字做加一操作如下

Producer類代碼

[java] view plain copy print?
  1. package com.crazycoder2010.thread;
  2. public class Producer implements Runnable{
  3. private TaskQueue taskQuery;
  4. public Producer(TaskQueue taskQuery){
  5. this.taskQuery = taskQuery;
  6. new Thread(this).start();
  7. }
  8. @Override
  9. public void run() {
  10. int i = 0;
  11. while(true){
  12. taskQuery.put(i++);
  13. }
  14. }
  15. }

消費者對象也是基於線程實現,在循環中不停的輪訓TaskQuery.get()來獲取當前對象

Consumer類

[java] view plain copy print?
  1. package com.crazycoder2010.thread;
  2. public class Consumer implements Runnable {
  3. private TaskQueue taskQuery;
  4. public Consumer(TaskQueue taskQuery){
  5. this.taskQuery = taskQuery;
  6. new Thread(this).start();
  7. }
  8. @Override
  9. public void run() {
  10. while(true){
  11. taskQuery.get();
  12. }
  13. }
  14. }

運行一下,看看結果是否是我們所期望的那樣

Launcher類代碼

[java] view plain copy print?
  1. package com.crazycoder2010.thread;
  2. public class Launcher {
  3. public static void main(String[] args) {
  4. TaskQueue taskQuery = new TaskQueue();
  5. new Producer(taskQuery);
  6. new Consumer(taskQuery);
  7. System.out.println("Press Control-C to stop.");
  8. }
  9. }

輸出結果:

...

Put:58

Put:59

Put:60

Put:61

Put:62

Put:63

Put:64

Got:64

Got:64

Got:64

Put:65

Put:66

...

問題出現在哪裏呢?

從結果輸出我們可以看出,生產者的對象放入順序是按次序進行的,但是消費者讀取對象的數序很奇怪,一段時間內讀取同一個數值,這個是怎麽造成的呢?

我們知道,當啟動線程後線程什麽時候被jvm所調度是不確定的,上面的結果在不同的機器上運行很可能得到不同的結果,當Producer線程被調度運行一段時間後,線程Consumer獲取到運行資格,然後從TaskQueue中取對象,這個時候由於Producer處於等待被調度狀態,所以TaskQueue中的id一直都是個固定值,所以這個時候Consumer獲取到的一直都是Producer在被jvm設置為等待狀態那一刻的值,運行一段時間,Producer又被jvm調度器調度,獲取運行資格,這個時候id繼續從上次暫定時的值開始累加,依次循環,然後就得到了上面的運行結果

方案2

這個方案裏我們要對方案1做一些改造,當有Producer生產出一個id時,直到有ConSumer來將他拿走,然後再生產下一個id,Consumer也是類似,直到Producer生產出id後才來取,否則一直等待下去

解決:

Object類裏有個wait()方法和notify()/notifyAll(),一直很神秘,先前沒怎麽用過,看了一下原來這個東東是與線程同步操作密接相關的,

比如我們在應用中如果要對某一個方法或某個代碼段做線程同步控制,那麽需要為這個方法添加synchronized修飾或者是synchronized(obj){},這個obj可以理解成我們實際生活中房間的一把鎖,默認情況下,當一個線程擁有了一個方法或代碼段的鎖後,就可以進入房間(方法或代碼塊)任意幹壞事,而其他的線程只能在門外等待直到當前線程執行完畢打開房間(釋放鎖),而Object的wait和notify則是給這個鎖提供了一些更先進的功能,這個鎖可以自己開鎖wait()(讓同步方法或代碼塊暫時放棄占用的鎖),進而讓別的線程有機會進入運行,而當實際成熟時(滿足運行的條件)又可以把自身鎖住notify(),進而又繼續進入上次被暫停的操作

改造後的TaskQueue2

[java] view plain copy print?
  1. package com.crazycoder2010.thread;
  2. public class TaskQuery2 {
  3. private int id;
  4. private boolean valueSet;
  5. public synchronized int get(){
  6. if(!valueSet){
  7. try {
  8. wait();
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. valueSet = false;
  14. notify();
  15. System.out.println("Got:"+this.id);
  16. return this.id;
  17. }
  18. public synchronized void put(int id){
  19. if(valueSet){
  20. try {
  21. wait();
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. this.id = id;
  27. valueSet = true;
  28. System.out.println("Put:"+id);
  29. notify();
  30. }
  31. }

Producer,Consumer和Launcher類的代碼不改動

運行結果如下

Put:0

Got:0

Put:1

Got:1

Put:2

Got:2

Put:3

Got:3

Put:4

Got:4

Put:5

Got:5

Put:6

Got:6

基於線程實現的生產者消費者模型(Object.wait(),Object.notify()方法)