多线程之银行排队叫号系统的实现

一、问题描述

去银行办理业务时,银行有固定的柜台数量,然后不定时有客户来银行办理业务。来的时候,客户先取号,再等着排队叫号。为了保证公平,叫号时要按照排队顺序叫,不能插队。

二、问题分析

这其实是一个典型的生产者消费者模式,其中柜台是消费者,而顾客是生产者。为了保存客户的排队状态,需要使用一个先进先出的队列来作为缓冲区。客户(生产者)来了以后,就进缓冲区排队。而每个柜台每办理完一个客户的业务,就从缓冲区中取出一个客户继续办理业务。

三、实现

分析清楚以后,来进行实现。

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();
    }

}

最后的输出为:
多线程之银行排队叫号系统的实现_第1张图片

3.5 讨论

  • 1.上面加锁是通过synchronized实现的,也可以用重入锁实现(《实战Java高并发程序设计》中说,JDK1.6以后的Java版本synchronized和重入锁性能基本相近,故此处用synchronized性能应该也一定会慢)。但是。重入锁可以设置公平锁,这样就能一定程度上避免使用synchronized时,一个柜台一直在办理业务,而另一个柜台在闲置的情况。
  • 2.此处排队是完全公平的,有时候实际情况可能会有需要插队的情况出现(比如VIP客户),此时可以将LinkedList换成PriorityQueue

你可能感兴趣的:(Java后端)