這是一個面試經常被問到的問題,很多問題都可以轉化為這個模型。
什么是生產者與消費者問題?舉個例子,我們去吃自助餐,在自助餐的一個公共區域放着各種食物,消費者需要就自行挑選,當食物被挑沒的時候,大家就等待,等候廚師做出更多再放到公共區域內供大家挑選;當公共區域食物達到一定數量,不能再存放的時候,此時沒有消費者挑選,廚師此時等待,等公共區域有地方再存放食物時,再開始生產。這就是一個生產者與消費者問題。
根據這個例子,我們可以模擬一下場景,我們從這個例子中,顯然看出我們需要制造一個公共區域,而且這個公共區域是有容量限制的,需要模擬各種食物,同時還需要模擬幾個廚師也就是生產者,最后再模擬幾個消費者。
首先呢,我們創建一個產品Product類,這個類就代表食物的模板,廚師們就生產這種類型的食物,類里面定義食物的ID和name這兩個屬性,代碼如下:
public class Product { private int id; private String name; public Product(int id, String name) { super(); this.id = id; this.name = name; } @Override public String toString() { return "Product :id:"+id+",name:"+name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
接下來,我們創建那個公共區域,也就是一個容器,容器要么用數組保存,要么用集合,這里我們用集合LinkedList,然后呢,我們要清楚我們這個容器只能被創建一個,也就是公共區域只有一個,我們放也是放在這里面,取也是從這里面取,這里采用單例模式來保證只能創建一個容器實例,容器里面再定義放和取的方法,我們要加鎖保證放和取的同步,以免發生線程安全問題,同時還要定義容器的最大容量,再放和取的同步方法里面,我們要判斷是否容器內的食物超出了容器的最大容量,如果放的時候大於等於最大容量了,就不能再往里面放了,這時候廚師們(生產者)要等待;取的時候也一樣,看是否容器內的食物個數為0,為0消費者們就要等待,最后我們在容器里面定義一個檢查容器容量的方法,后面單獨開啟一個線程調用此方法,實時監測容器內食物的個數,一直在0-10之間,就說明我們寫的代碼沒有問題,代碼如下:
public class Container { //單例模式,保證只能創建一個容器實例 private static Container instance = null; private Container(){} public static Container getInstance(){ if(instance == null){ instance = new Container(); } return instance; } private LinkedList<Product> list = new LinkedList<>();//容器 private int MAX = 10;//容器的最大容量 //放食物 public synchronized void putProduct(Product product){ while(list.size()>=MAX){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.addLast(product);//把新生產的食物放在最后 notifyAll(); } //取食物 public synchronized Product getProduct(){ while(list.size()<=0){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } notifyAll(); return list.removeFirst();//先取出放在這里時間最長的那個 } //檢查容器內食物個數 public int checkSize(){ return list.size(); } }
接下來就是生產者(廚師)線程,這個線程里面就是不停的生產食物,然后放入容器里面供消費者挑選,更形象一點,生產食物需要固定的時間,我們讓線程每生產一個食物睡眠一段時間,時間為固定的。
代碼如下:
public class ProducerThread implements Runnable{ Container container = null; public ProducerThread(Container container) { this.container = container; } @Override public void run() { int i = 0; while(true){ Product product = new Product(++i, "產品"+i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } container.putProduct(product); } } }
有了生產者,接下來就是消費者線程了,該線程里面消費者不停的挑選產品,消費產品,每消費一個食物也需要一定的時間,而且這個對於每一個人時間是不一定的,所以呢我們就取1200以內的隨機數,進行睡眠。
代碼如下:
public class CustomThread implements Runnable{ Container container = null; public CustomThread(Container container) { this.container = container; } @Override public void run() { while(true){ Product p = container.getProduct(); try { Thread.sleep((int)(Math.random()*1200)); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("消費了一個產品"+p); } } }
在生產者與消費者線程里面,我們都定義了構造器,用來接收容器對象,這里面,並沒有對容器進行初始化,就是為了保證容器的唯一性。
所需要的我們都定義完畢,最后定義一個測試類Go,測試類里面,創建2個生產者線程,10個消費者線程,並創建一個容器傳給生產者和消費者,同時,另外開啟一個線程,每隔300毫秒進行輸出容器內食物的數量,如果食物數量一直在0到10之間,說明我們的程序沒有問題,為了讓打印看的更清晰,這里不用System.out.println打印,而是用顯示為紅色的System.err.println進行輸出。
代碼如下:
public class Go { public static void main(String[] args) { Container container = Container.getInstance(); for (int i = 0; i < 2; i++) { new Thread(new ProducerThread(container)).start();; } for (int i = 0; i < 10; i++) { new Thread(new CustomThread(container)).start();; } new Thread(new Runnable() { @Override public void run() { while(true){ try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.err.println(container.checkSize()); } } }).start(); } }
整個生產者和消費者就完成了。
這里面一定要思考,代碼的同步,容器的唯一以及對容器容量的控制,之間是怎么進行通訊的。
