1. 程式人生 > >四、生產者和消費者

四、生產者和消費者

tar 訪問 正在執行 try col logs lsp rep 一定的

我們這裏的生產者和消費者模型為:

    生產者Producer 生產某個對象(共享資源),放在緩沖池中,然後消費者從緩沖池中取出這個對象。也就是生產者生產一個,消費者取出一個。這樣進行循環。

  第一步:我們先創建共享資源的類 Person,它有兩個方法,一個生產對象,一個消費對象

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Person { private String name; private int age; /** * 生產數據
* @param name * @param age */ public void push(String name,int age){ this.name = name; this.age = age; } /** * 取數據,消費數據 * @return */ public void pop(){ System.out.println(this.name+"---"+this.age); } }

  第二步:創建生產者線程,並在 run() 方法中生產50個對象

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Producer implements Runnable{ //共享資源對象 Person p = null; public Producer(Person p){ this.p = p; } @Override public void run() { //生產對象 for(int i = 0 ; i < 50 ; i++){ //如果是偶數,那麽生產對象 Tom--11;如果是奇數,則生產對象 Marry--21
if(i%2==0){ p.push("Tom", 11); }else{ p.push("Marry", 21); } } } }

  第三步:創建消費者線程,並在 run() 方法中消費50個對象

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Consumer implements Runnable{ //共享資源對象 Person p = null; public Consumer(Person p) { this.p = p; } @Override public void run() { for(int i = 0 ; i < 50 ; i++){ //消費對象 p.pop(); } } }

  由於我們的模型是生產一個,馬上消費一個,那期望的結果便是 Tom---11,Marry--21,Tom---11,Mary---21...... 連續這樣交替出現50次

但是結果卻是:

1 2 3 4 5 6 7 8 9 10 11 Marry---21 Marry---21 Marry---21 Marry---21 Marry---21 ...... Marry---21 Marry---21 Marry---21 Marry---21 Marry---21

為了讓結果產生的更加明顯,我們在共享資源的 pop() 和 push() 方法中添加一段延時代碼

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * 生產數據 * @param name * @param age */ public void push(String name,int age){ this.name = name; try { //這段延時代碼的作用是可能只生產了 name,age為nul,消費者就拿去消費了 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } this.age = age; } /** * 取數據,消費數據 * @return */ public void pop(){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.name+"---"+this.age); }  

  

這個時候,結果如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Marry---11 Tom---21 Marry---11 Tom---21 Marry---11 Tom---21 Marry---11 Tom---21 ...... Tom---11 Tom---21 Marry---11 Tom---21 Marry---11 Marry---21

  

結果分析:這時候我們發現結果全亂套了,Marry--21是固定的,Tom--11是固定的,但是上面的結果全部亂了,那這又是為什麽呢?而且有很多重復的數據連續出現,那這又是為什麽呢?

原因1:出現錯亂數據,是因為先生產出Tom--11,但是消費者沒有消費,然後生產者繼續生產出name為Marry,但是age還沒有生產,而消費者這個時候拿去消費了,那麽便出現 Marry--11。同理也會出現 Tom--21

原因2:出現重復數據,是因為生產者生產一份數據了,消費者拿去消費了,但是第二次生產者生產數據了,但是消費者沒有去消費;而第三次生產者繼續生產數據,消費者才開始消費,這便會產生重復

解決辦法1:生產者生產name和age必須要是一個整體一起完成,即同步。生產的中間不能讓消費者來消費即可。便不會產生錯亂的數據。如何同步可以參考:

        Java 多線程詳解(三)------線程的同步:http://www.cnblogs.com/ysocean/p/6883729.html

     這裏我們選擇同步方法(在方法前面加上 synchronized)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Person { private String name; private int age; /** * 生產數據 * @param name * @param age */ public synchronized void push(String name,int age){ this.name = name; try { //這段延時代碼的作用是可能只生產了 name,age為nul,消費者就拿去消費了 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } this.age = age; } /** * 取數據,消費數據 * @return */ public synchronized void pop(){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.name+"---"+this.age); } }

  結果如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 Marry---21 Marry---21 Marry---21 Marry---21 Marry---21 Tom---11 Tom---11 ...... Tom---11 Tom---11 Tom---11 Tom---11 Tom---11

問題:還是沒有解決上面的問題2,出現重復的問題。期望的結果是 Tom---11,Marry--21,Tom---11,Mary---21...... 連續這樣交替出現50次。那如何解決呢?

解決辦法:生產者生產一次數據了,就暫停生產者線程,等待消費者消費;消費者消費完了,消費者線程暫停,等待生產者生產數據,這樣來進行。

這裏我們介紹一個同步鎖池的概念:

  同步鎖池:同步鎖必須選擇多個線程共同的資源對象,而一個線程獲得鎖的時候,別的線程都在同步鎖池等待獲取鎖;當那個線程釋放同步鎖了,其他線程便開始由CPU調度分配鎖

關於讓線程等待和喚醒線程的方法,如下:(這是 Object 類中的方法)

  技術分享圖片

  技術分享圖片

wait():執行該方法的線程對象,釋放同步鎖,JVM會把該線程放到等待池中,等待其他線程喚醒該線程

notify():執行該方法的線程喚醒在等待池中等待的任意一個線程,把線程轉到鎖池中等待(註意鎖池和等待池的區別)

notifyAll():執行該方法的線程喚醒在等待池中等待的所有線程,把線程轉到鎖池中等待。

註意:上述方法只能被同步監聽鎖對象來調用,這也是為啥wait() 和 notify()方法都在 Object 對象中,因為同步監聽鎖可以是任意對象,只不過必須是需要同步線程的共同對象即可,否則別的對象調用會報錯:        java.lang.IllegalMonitorStateException

假設 A 線程和 B 線程同時操作一個 X 對象,A,B 線程可以通過 X 對象的 wait() 和 notify() 方法來進行通信,流程如下:

①、當線程 A 執行 X 對象的同步方法時,A 線程持有 X 對象的 鎖,B線程在 X 對象的鎖池中等待

②、A線程在同步方法中執行 X.wait() 方法時,A線程釋放 X 對象的鎖,進入 X 對象的等待池中

③、在 X 對象的鎖池中等待鎖的 B 線程獲得 X 對象的鎖,執行 X 的另一個同步方法

④、B 線程在同步方法中執行 X.notify() 方法,JVM 把 A 線程從等待池中移動到 X 對象的鎖池中,等待獲取鎖

⑤、B 線程執行完同步方法,釋放鎖,等待獲取鎖的 A 線程獲得鎖,繼續執行同步方法

那麽為了解決上面重復的問題,修改代碼如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class Person { private String name; private int age; //表示共享資源對象是否為空,如果為 true,表示需要生產,如果為 false,則有數據了,不要生產 private boolean isEmpty = true; /** * 生產數據 * @param name * @param age */ public synchronized void push(String name,int age){ try { //不能用 if,因為可能有多個線程 while(!isEmpty){//進入到while語句內,說明 isEmpty==false,那麽表示有數據了,不能生產,必須要等待消費者消費 this.wait();//導致當前線程等待,進入等待池中,只能被其他線程喚醒 } //-------生產數據開始------- this.name = name; //延時代碼 Thread.sleep(10); this.age = age; //-------生產數據結束------- isEmpty = false;//設置 isEmpty 為 false,表示已經有數據了 this.notifyAll();//生產完畢,喚醒所有消費者 } catch (Exception e) { e.printStackTrace(); } } /** * 取數據,消費數據 * @return */ public synchronized void pop(){ try { //不能用 if,因為可能有多個線程 while(isEmpty){//進入 while 代碼塊,表示 isEmpty==true,表示為空,等待生產者生產數據,消費者要進入等待池中 this.wait();//消費者線程等待 } //-------消費開始------- Thread.sleep(10); System.out.println(this.name+"---"+this.age); //-------消費結束------ isEmpty = true;//設置 isEmpty為true,表示需要生產者生產對象 this.notifyAll();//消費完畢,喚醒所有生產者 } catch (InterruptedException e) { e.printStackTrace(); } } }

  

結果:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Tom---11 Marry---21 Tom---11 Marry---21 Tom---11 Marry---21 Tom---11 ...... Marry---21 Tom---11 Marry---21 Tom---11 Marry---21 Tom---11 Marry---21  

那麽這便是我們期待的結果,交替出現。

死鎖:

①、多線程通信的時候,很容易造成死鎖,死鎖無法解決,只能避免

②、當 A 線程等待由 B 線程持有的鎖,而 B 線程正在等待由 A 線程持有的鎖時發生死鎖現象(比如A拿著鉛筆,B拿著圓珠筆,A說你先給我圓珠筆,我就把鉛筆給你,而B說你先給我鉛筆,我就把圓珠筆給你,這就造成了死鎖,A和B永遠不能進行交換)

③、JVM 既不檢測也不避免這種現象,所以程序員必須保證不能出現這樣的情況

Thread 類中容易造成死鎖的方法(這兩個方法都已經過時了,不建議使用):

suspend():使正在運行的線程放棄 CPU,暫停運行(不釋放鎖)

resume():使暫停的線程恢復運行

情景:A 線程獲得對象鎖,正在執行一個同步方法,如果 B線程調用 A 線程的 suspend() 方法,此時A 暫停運行,放棄 CPU 資源,但是不放棄同步鎖,那麽B也不能獲得鎖,A又暫停,那麽便造成死鎖。

解決死鎖法則:當多個線程需要訪問 共同的資源A,B,C時,必須保證每一個線程按照一定的順序去訪問,比如都先訪問A,然後B,最後C。就像我們這裏的生產者---消費者模型,制定了必須生產者先生產一個對象,然後消費者去消費,消費完畢,生產者才能在開始生產,然後消費者在消費。這樣的順序便不會造成死鎖。

四、生產者和消費者