我們將生產者、消費者、庫存、和調用線程的主函數分別寫進四個類中,通過搶奪非線程安全的數據集合來直觀的表達在進行生產消費者模型的過程中可能出現的問題與解決辦法。 我們假設有一個生產者,兩個消費者來共同搶奪庫存里的資源,而生產者和消費者都以線程來實現。 庫存對象只有是唯一的才會出現搶奪一個資源的可能,所 ...
我們將生產者、消費者、庫存、和調用線程的主函數分別寫進四個類中,通過搶奪非線程安全的數據集合來直觀的表達在進行生產消費者模型的過程中可能出現的問題與解決辦法。
我們假設有一個生產者,兩個消費者來共同搶奪庫存里的資源,而生產者和消費者都以線程來實現。
庫存對象只有是唯一的才會出現搶奪一個資源的可能,所以為了使庫存對象是唯一的,我們可以使用兩種方法實現,單例模式和通過生產者和消費者的構造函數參數來初始化。
本次舉例使用的是構造函數的方法,但代碼中也註釋出了單例模式的寫法與使用。
先創建一個簡單的生產消費者模型,查看它的運行結果。
-
庫存類:
package producterac; import java.util.ArrayList; public class WareHouse { //存放非線程安全的數組的集合 private ArrayList<String> list = new ArrayList<String>(); /* * //創建單例模式使生產消費者操作的是同一庫存對象 * private WareHouse() {} * //建立靜態對象以在初始化的時候建立僅一個庫存對象 * private static WareHouse wh = new WareHouse(); * * //將方法設置為靜態是因為在無法new庫存對象的情況下, * //我們可以通過將方法設定為靜態來直接通過類名調用靜態方法 * public static WareHouse getInstance() { * return wh; * } */ //寫生產者操作倉庫的方法 public void add() { if(list.size() < 20) { list.add("一個數據"); }else { //數據存夠之後直接返回,不運行存儲數據的操作 return; } } //寫消費者操作倉庫的操作 public void get() { //判斷集合中是否還有數據可以取出 //如果不判斷會造成集合越界 if(list.size() > 0) { list.remove(0); }else { return; } } }
- 生產者類:
package producterac; public class Producter extends Thread{ private String pName; //我們要使生產者和消費者操控同一個庫存對象 //也可以使用單例模式來建立庫存對象 private WareHouse wh; public Producter(String pName,WareHouse wh) { this.pName = pName; this.wh = wh; } //重寫run方法 public void run() { while(true) { wh.add(); System.out.println("生產者"+pName+"添加了一個貨物"); try { //使線程等待一會兒 Thread.sleep(200); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
- 消費者類:
package producterac; public class Consumer extends Thread{ private String cName; //獲取庫存對象 /* private WareHouse wh = WareHouse.getInstance(); */ //我們要使生產者和消費者操控同一個庫存對象 //也可以使用單例模式來建立庫存對象 private WareHouse wh; public Consumer(String cName,WareHouse wh) { this.cName = cName; this.wh = wh; } //重寫run方法 public void run() { while(true) { wh.get(); System.out.println("消費者"+cName+"拿走了一個貨物"); try { //使線程等待一會兒 Thread.sleep(200); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
- 主函數類:
package producterac; public class Main { public static void main(String[] args) { WareHouse wh = new WareHouse(); Producter p1 = new Producter("1", wh); Consumer c1 = new Consumer("1", wh); Consumer c2 = new Consumer("2", wh); p1.start(); c1.start(); c2.start(); } }
部分運行結果:
我們看到出現 java.lang.ArrayIndexOutOfBoundsException異常,說明消費者在拿走貨物的時候集合越界沒有拿到,所以出現了異常。
即使我們在庫存的get()方法中判斷了集合是否為空,但也還是出現了異常。原因是因為在兩個線程同時訪問一個對象的時候,有可能當線程1剛判斷完集合不為空進入了if迴圈但還沒有拿走貨物的情況下,線程2也進行了get()方法先線程1一步拿走了最後的一個貨物,然後當線程1想拿走貨物的時候集合里已經沒有了,這種情況下就會發生上述異常。
這就造成了線程搶奪資源時非安全的問題,那麼我們可以將庫存對象使用線程鎖synchronized鎖起來,這樣在一個消費者訪問庫存對象的時候其他消費者無法訪問庫存對象,從而解決集合越界問題,使線程安全。
- 修改過的庫存類(加入了synchronized修飾符的add()和get()方法):
//寫生產者操作倉庫的方法 public synchronized void add() { if(list.size() < 20) { list.add("一個數據"); }else { //數據存夠之後直接返回,不運行存儲數據的操作 return; } } //寫消費者操作倉庫的操作 public synchronized void get() { //判斷集合中是否還有數據可以取出 //如果不判斷會造成集合越界 if(list.size() > 0) { list.remove(0); }else { return; } }
使用synchronized修飾符修飾庫存方法之後就不會報錯了!
我們也可以將return替換為wait()方法讓線程等待,將編寫的生產消費者模型中的return修改為wait()。
- 修改過的庫存類:
//寫生產者操作倉庫的方法 public synchronized void add() { if(list.size() < 20) { list.add("一個數據"); }else { try { //這個this指的是訪問庫存對象的線程wait,不是庫存對象wait this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } //寫消費者操作倉庫的操作 public synchronized void get() { //判斷集合中是否還有數據可以取出 //如果不判斷會造成集合越界 if(list.size() > 0) { list.remove(0); }else { try { //這個this指的是訪問庫存對象的線程wait,不是庫存對象wait this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
運行結果:
我們會發現到最後所有的線程都會處於wait等待狀態,運行到最後沒有線程在執行了。所以我們需要在其中一個線程等待的時候將其他線程繼續喚醒,保持系統的運行。
喚醒線程可以使用notify/notifyAll()方法。
- 再次修改後的庫存類:
//寫生產者操作倉庫的方法 public synchronized void add() { if(list.size() < 20) { list.add("一個數據"); }else { try { //因為我們無法知道哪個線程是消費者線程,所以我們要將線程全部喚醒 this.notifyAll(); //這個this指的是訪問庫存對象的線程wait,不是庫存對象wait this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } //寫消費者操作倉庫的操作 public synchronized void get() { //判斷集合中是否還有數據可以取出 //如果不判斷會造成集合越界 if(list.size() > 0) { list.remove(0); }else { try { //因為我們無法知道哪個線程是生產者線程,所以我們要將線程全部喚醒 this.notifyAll(); //這個this指的是訪問庫存對象的線程wait,不是庫存對象wait this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
運行成功!說明我們這時候真正地實現了簡單的生產消費者模型。
附:如果將完成的生產消費者模型中add()和get()方法的synchronized修飾符去掉,會發生如下錯誤。
將synchronized修飾符去掉後,發生了java.lang.IllegalMonitorStateException異常,原因是當線程1進入else要執行wait()方法的那個時刻,線程2也進入了庫存對象中,致使當wait()方法真正執行的時候wait的是線程2而不是線程1,發生這種情況的時候就會發生上述異常。