生產者消費者問題
這是一個面試經常被問到的問題,很多問題都可以轉化為這個模型。
什麼是生產者與消費者問題?舉個例子,我們去吃自助餐,在自助餐的一個公共區域放著各種食物,消費者需要就自行挑選,當食物被挑沒的時候,大家就等待,等候廚師做出更多再放到公共區域內供大家挑選;當公共區域食物達到一定數量,不能再存放的時候,此時沒有消費者挑選,廚師此時等待,等公共區域有地方再存放食物時,再開始生產。這就是一個生產者與消費者問題。
根據這個例子,我們可以模擬一下場景,我們從這個例子中,顯然看出我們需要製造一個公共區域,而且這個公共區域是有容量限制的,需要模擬各種食物,同時還需要模擬幾個廚師也就是生產者,最後再模擬幾個消費者。
首先呢,我們建立一個產品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(); } }
整個生產者和消費者就完成了。
這裡面一定要思考,程式碼的同步,容器的唯一以及對容器容量的控制,之間是怎麼進行通訊的。