一、問題描述
去銀行辦理業務時,銀行有固定的櫃檯數量,然後不定時有客戶來銀行辦理業務。來的時候,客戶先取號,再等着排隊叫號。爲了保證公平,叫號時要按照排隊順序叫,不能插隊。
二、問題分析
這其實是一個典型的生產者消費者模式,其中櫃檯是消費者,而顧客是生產者。爲了保存客戶的排隊狀態,需要使用一個先進先出的隊列來作爲緩衝區。客戶(生產者)來了以後,就進緩衝區排隊。而每個櫃檯每辦理完一個客戶的業務,就從緩衝區中取出一個客戶繼續辦理業務。
三、實現
分析清楚以後,來進行實現。
3.1 緩衝區的實現
我們實現首先實現緩衝區,代碼如下:
import java.util.LinkedList;
import java.util.Queue;
public class CallHall {
private static CallHall callHall;
private final int MAX_NUM=999;//叫到的最大號
/**
* 單例獲取方式
* @return
*/
public static CallHall getInstance(){
if (callHall ==null){
callHall =new CallHall();
}
return callHall;
}
private CallHall() {
}
private int lastNumber = 1;//當前最後一個客戶的號碼
private Queue<Integer> queue = new LinkedList<>();//叫號隊列
/**
* 新加入一個客戶
* @return
*/
public synchronized Integer generateNewManager() {
if (lastNumber>MAX_NUM)//當叫號達到最大號碼,從1重新開始排號
lastNumber=1;
queue.offer(lastNumber);
return lastNumber++;
}
/**
* 從緩衝區中取一個客戶出來
* @return
*/
public synchronized Integer fetchServiceNumber() {
Integer number = null;
if (!queue.isEmpty()) {
number = queue.remove();
}
return number;
}
}
因爲緩衝區在生產者和消費者都都會用到,所以以單例模式設置緩衝區。其中,使用一個LinkedList
來做爲隊列,lastNumber
用來標記最新來的客戶所取得號碼。注意的事情有兩點:
- 1.爲了保證線程安全,給緩衝區加入數據、從緩衝區中取出數據的時候都要用synchronized進行同步。
- 2.爲防止叫號過大,當叫號大於一個值(這裏設置的999)的時候,要從頭(這裏是1)開始叫號。
3.2 消費者的實現
這裏的消費者,就是櫃檯,其作用,不斷輪詢從緩衝區中取排號出來辦理業務。實現代碼如下:
/***
* 銀行櫃檯實體
*/
public class Counter {
private int counterId;
private ITask task;
public Counter(int counterId,ITask task) {
this.counterId = counterId;
this.task=task;
}
/**
* 開闢輪詢線程開始輪詢
*/
public void start() {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
while (true) {
task.doTask(counterId);
}
}
});
thread.start();
}
}
其實消費者就一個方法start
,作用就是開啓一個輪詢線程,不斷從緩衝區中取數據出來處理。爲了代碼以後擴展方便,此處設計了一個ITask
接口,通過接口來調用。ITask的定義如下:
public interface ITask {
/**
* 執行任務的方法
* @param counterID 執行此任務的櫃檯的ID
*/
void doTask(int counterID);
}
其實裏面也只有一個方法doTask
,這裏傳入櫃檯的ID作爲參數,以區別是在哪個櫃檯處理Task。
再來定義一個類來實現ITask接口:
public class Task implements ITask {
@Override
public void doTask(int counterID) {
String counterName = counterID + "號櫃檯";
System.out.println(counterName + "正在獲取任務...");
Integer number = CallHall.getInstance().fetchServiceNumber();
if (number != null) {
System.out.println(counterName + "正在爲" + number + "號客戶提供服務");
int serviceTime = (int)(new Random().nextDouble()*5000);//服務時間在0-2s之間
try {
Thread.sleep(serviceTime);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(counterName + "爲" + number + "號客戶完成服務,耗時" + serviceTime/1000+ "秒");
} else {
try {
System.out.println(counterName + "沒有客戶,休息1秒");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
可以看到,在doTask方法中,首先去緩衝區中取數據,如果沒有,就sleep一秒。如果取到數據,就辦理業務並輸出。
3.3 生產者的實現
生產者即來銀行辦理業務的客戶,爲了真實模擬,我們假設在銀行剛開門的時候,有一批排隊在銀行門口的人。等銀行開門後,在某一時間,會有n個人(這裏是1-3個人)前來銀行辦理業務。代碼如下:
/**
* 模擬的消費者類,模擬銀行客戶的狀況
*/
public class Consumer implements Runnable{
@Override
public void run() {
//一大早銀行開門,湧進來20個人
for (int i=0;i<20;++i){
CallHall.getInstance().generateNewManager();
}
//然後,每隔一個隨機時間來隨機個人數
Random random=new Random();
while (true){
int sleepTime=(int)(random.nextFloat()*5000);//來人的間隔時間
int peopleNum=random.nextInt(3)+1;//來銀行的人數:1-3個之間
for (int i=0;i<peopleNum;++i){
CallHall.getInstance().generateNewManager();
}
try {
Thread.sleep(sleepTime);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
3.4 模擬
創建生產者(此處是4個櫃檯)、緩衝區、消費者來模擬,代碼如下:
public class MainClass {
public static void main(String[] args) {
//創建4個櫃檯
for (int i = 0; i < 4; i++) {
Counter commonWindow = new Counter(i,new Task());
commonWindow.start();
}
//模擬銀行客戶來的過程
new Thread(new Consumer()).start();
}
}
最後的輸出爲:
3.5 討論
- 1.上面加鎖是通過
synchronized
實現的,也可以用重入鎖實現(《實戰Java高併發程序設計》中說,JDK1.6以後的Java版本synchronized和重入鎖性能基本相近,故此處用synchronized
性能應該也一定會慢)。但是。重入鎖可以設置公平鎖,這樣就能一定程度上避免使用synchronized
時,一個櫃檯一直在辦理業務,而另一個櫃檯在閒置的情況。 - 2.此處排隊是完全公平的,有時候實際情況可能會有需要插隊的情況出現(比如VIP客戶),此時可以將
LinkedList
換成PriorityQueue
。