多線程之銀行排隊叫號系統的實現

一、問題描述

去銀行辦理業務時,銀行有固定的櫃檯數量,然後不定時有客戶來銀行辦理業務。來的時候,客戶先取號,再等着排隊叫號。爲了保證公平,叫號時要按照排隊順序叫,不能插隊。

二、問題分析

這其實是一個典型的生產者消費者模式,其中櫃檯是消費者,而顧客是生產者。爲了保存客戶的排隊狀態,需要使用一個先進先出的隊列來作爲緩衝區。客戶(生產者)來了以後,就進緩衝區排隊。而每個櫃檯每辦理完一個客戶的業務,就從緩衝區中取出一個客戶繼續辦理業務。

三、實現

分析清楚以後,來進行實現。

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章