首先,为什么要做这个案例?一方面是我想给大家介绍一下在多线程中wait、notify、notifyAll的简单用法和一些注意事项,另一个方面是我自己也想研究这种餐厅排队系统的实现原理。这个案例仅代表我个人的一些比较的简单想法,具体的更复杂业务并未涉及。但最基本的几个业务还是要有的。这个案例实现的几个简单的基本业务如下:

一、餐厅座位管理

餐厅的座位是可以变化的,在这里我给了一个变量seatNumber可以动态修改餐厅座位数量。当然,在客户就餐后我们需要记录的是客户就餐当时的已使用座位数useNumber,以便于后期通过剩余座位号来实现叫号逻辑。

二、用户手动取号

用户手动取号,用户具体是如何取号。这里我并不关心,取号的方式有多种方式,微信公众号取号,小程序取号等等。主要是后台实现用户取号的过程。

我在这里是通过一个线程代表一个客户,以线程的方式绑定共享的排队号码容器,这个可以使用ThreadLocal类来实现。当然也可以使用Map容器storage来绑定。在这里,我使用的是后者。假设有这么一个容器,我们的客户和他取到的号码就能绑定在一起。那么取号过程中,也需要一个可以一直递增的号码takeNumber,后续取号将一直使用。

三、系统自动叫号

系统自动叫号,需要两个变量,最新叫号curCallNumber,也可以说是当前叫号。上次叫号prevCallNumber。叫号过程中会结合座位剩余和这两个变量。

四、餐厅收桌维护

餐厅运营过程中,座位是会重复清理干净给新客户使用,这个过程也要加入到整个系统实现,以维持整个排队容器正常使用。

当然还有用户延号问题等等,这里我就不一一给大家分析。接下来我们主要来实现上面的几个基本业务。

代码实现如下:

主要几个业务方法都在Counter类下,为了线程安全,我在每个业务方法上加synchronized关键字。

而且wait、notify、notifyAll都需要在加锁的前提下使用

/**
 * @author 小吉
 * @description 简单自动排队实现
 * @date 2020/7/5
 */
public class Counter {

    private int takeNumber;//取号
    private int curCallNumber;//最新叫号
    private int prevCallNumber;//上次叫号
    private int useNumber;//餐厅已使用座位数
    private int seatNumber;//餐厅总座位数
    private Map<String,Integer> storage;//排队号码

    public Counter(int seatNumber){
        this.seatNumber = seatNumber;
        this.storage = new HashMap<>();
    }

    /**
     * 取号
     */
    public synchronized void takeNumber(){
        String name = Thread.currentThread().getName();
        try {
            //模拟用户取号
            storage.put(name,++takeNumber);
            System.out.println("您好," + name + "!取号成功,您的号码是" + takeNumber + ",当前叫号是" + curCallNumber + ",前面还有" + (takeNumber - curCallNumber - 1) + "桌");
            //当前用户手持号码大于最新叫号则需要等待
            while (storage.get(name) > curCallNumber){
                //模拟用户等待时间过长时获取进度(wait超时机制)
                wait((new Random().nextInt(60) + 1) * 1000);
                if(storage.get(name) > curCallNumber){
                    getCurrentProgress();
                }
            }
            for(int i = prevCallNumber + 1;i <= curCallNumber;i++){
                if(storage.get(name) == i){
                    storage.remove(name);
                    useNumber++;
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取用户当前进度
     */
    public synchronized void getCurrentProgress(){
        String name = Thread.currentThread().getName();
        if(storage.get(name) != null){
            int curNumber = storage.get(name);
            System.out.println("您好," + name + "!您的号码是" + curNumber + ",当前叫号是" + curCallNumber + ",前面还有" + (curNumber - curCallNumber - 1) + "桌");
        }
//        else{
//            System.out.println("您好," + name + "!您已就餐,目前暂无排队侯餐记录!");
//        }
    }

    /**
     * 收桌
     */
    public synchronized void cutUseNumber(){
        if(useNumber > 0){
            System.out.println("服务员正在收桌...");
            useNumber--;
        }
    }

    /**
     * 叫号
     */
    public synchronized void callNumber(){
        /**
         * 已使用座位数小于总座位数则可叫号
         */
        if(useNumber < seatNumber){
            //保存上次叫号
            prevCallNumber = curCallNumber;
            //保存最新叫号
            for(int i = 0;i < seatNumber - useNumber;i++){
                if(curCallNumber < takeNumber){
                    System.out.println("请" + (++curCallNumber) + "号用户就餐!");
                }
            }
            notifyAll();
        }
    }
}

我们再定义一个代表客户的类,这个客户的动作就是取号,并且等待时可以查询排队进度。为了还原每个客户取号的时间不同,可以使用Thread.sleepRandom来产生不同客户错开取号的情况。

/**
 * @author 小吉
 * @description 客户
 * @date 2020/7/5
 */
public class Customer implements Runnable{

    private Counter counter;

    public Customer(Counter counter){
        this.counter = counter;
    }

    @Override
    public void run() {
        try {
            Thread.sleep((new Random().nextInt(10) + 1) * 100);
            //模拟取号
            counter.takeNumber();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

定义一个前台,专门用来叫号。当然这里可能是前台的系统叫号。

/**
 * @author 小吉
 * @description 前台
 * @date 2020/7/5
 */
public class FrontDesk implements Runnable{

    private Counter counter;

    public FrontDesk(Counter counter){
        this.counter = counter;
    }

    /**
     * 系统叫号
     */
    @Override
    public void run() {
        System.out.println("模拟叫号服务开始...");
        try {
            while (true){
                Thread.sleep(100);//模拟叫号
                counter.callNumber();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

当然这里也少不了服务员,因为客户使用完的餐桌需要回收。

/**
 * @author 小吉
 * @description 服务员
 * @date 2020/7/5
 */
public class Waiter implements Runnable{

    private Counter counter;

    public Waiter(Counter counter){
        this.counter = counter;
    }

    /**
     * 收桌
     */
    @Override
    public void run() {
        System.out.println("模拟收桌服务开始...");
        try {
            while (true){
//                Thread.sleep((new Random().nextInt(10) + 1) * 1000);//模拟收桌
                Thread.sleep(100);//模拟收桌
                counter.cutUseNumber();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

最后需要一个统筹规划的平台,统一管理这些对象以及任务分配。

/**
 * @author 小吉
 * @description 排队平台
 * @date 2020/7/5
 */
public class Platform {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("模拟客户取号开始...");
        //餐厅总共10个座位
        Counter counter = new Counter(10);
        //模拟30个客户排队
        for(int i = 1;i <= 30;i++){
            Thread t = new Thread(new Customer(counter));
            t.setName("客户" + i);
            t.start();
        }
        //系统自动叫号
        new Thread(new FrontDesk(counter)).start();
        //服务员收桌
        new Thread(new Waiter(counter)).start();
    }
}

简单介绍一下,这里我模拟了30个客户,对应创建30个线程,共享同一个对象counter,为的是在这些线程工作的时候获得的锁是一样的锁。当然前台叫号和服务员也是共用这个对象counter

实现效果:

模拟客户取号开始...
模拟叫号服务开始...
模拟收桌服务开始...
您好,客户17!取号成功,您的号码是1,当前叫号是0,前面还有0桌
请1号用户就餐!
服务员正在收桌...
您好,客户4!取号成功,您的号码是2,当前叫号是1,前面还有0桌
您好,客户12!取号成功,您的号码是3,当前叫号是1,前面还有1桌
您好,客户22!取号成功,您的号码是4,当前叫号是1,前面还有2桌
您好,客户30!取号成功,您的号码是5,当前叫号是1,前面还有3桌
您好,客户28!取号成功,您的号码是6,当前叫号是1,前面还有4桌
请2号用户就餐!
请3号用户就餐!
请4号用户就餐!
请5号用户就餐!
请6号用户就餐!
您好,客户24!取号成功,您的号码是7,当前叫号是6,前面还有0桌
服务员正在收桌...
请7号用户就餐!
您好,客户1!取号成功,您的号码是8,当前叫号是7,前面还有0桌
您好,客户15!取号成功,您的号码是9,当前叫号是7,前面还有1桌
您好,客户27!取号成功,您的号码是10,当前叫号是7,前面还有2桌
服务员正在收桌...
请8号用户就餐!
请9号用户就餐!
请10号用户就餐!
您好,客户7!取号成功,您的号码是11,当前叫号是10,前面还有0桌
您好,客户13!取号成功,您的号码是12,当前叫号是10,前面还有1桌
您好,客户9!取号成功,您的号码是13,当前叫号是10,前面还有2桌
服务员正在收桌...
请11号用户就餐!
请12号用户就餐!
请13号用户就餐!
您好,客户14!取号成功,您的号码是14,当前叫号是13,前面还有0桌
您好,客户11!取号成功,您的号码是15,当前叫号是13,前面还有1桌
您好,客户16!取号成功,您的号码是16,当前叫号是13,前面还有2桌
您好,客户19!取号成功,您的号码是17,当前叫号是13,前面还有3桌
您好,客户25!取号成功,您的号码是18,当前叫号是13,前面还有4桌
服务员正在收桌...
请14号用户就餐!
请15号用户就餐!
您好,客户25!您的号码是18,当前叫号是15,前面还有2桌
您好,客户19!您的号码是17,当前叫号是15,前面还有1桌
您好,客户16!您的号码是16,当前叫号是15,前面还有0桌
您好,客户8!取号成功,您的号码是19,当前叫号是15,前面还有3桌
您好,客户20!取号成功,您的号码是20,当前叫号是15,前面还有4桌
您好,客户29!取号成功,您的号码是21,当前叫号是15,前面还有5桌
服务员正在收桌...
请16号用户就餐!
您好,客户29!您的号码是21,当前叫号是16,前面还有4桌
您好,客户20!您的号码是20,当前叫号是16,前面还有3桌
您好,客户8!您的号码是19,当前叫号是16,前面还有2桌
您好,客户19!您的号码是17,当前叫号是16,前面还有0桌
您好,客户25!您的号码是18,当前叫号是16,前面还有1桌
服务员正在收桌...
请17号用户就餐!
您好,客户25!您的号码是18,当前叫号是17,前面还有0桌
您好,客户8!您的号码是19,当前叫号是17,前面还有1桌
您好,客户20!您的号码是20,当前叫号是17,前面还有2桌
您好,客户29!您的号码是21,当前叫号是17,前面还有3桌
您好,客户2!取号成功,您的号码是22,当前叫号是17,前面还有4桌
您好,客户5!取号成功,您的号码是23,当前叫号是17,前面还有5桌
您好,客户10!取号成功,您的号码是24,当前叫号是17,前面还有6桌
您好,客户18!取号成功,您的号码是25,当前叫号是17,前面还有7桌
您好,客户26!取号成功,您的号码是26,当前叫号是17,前面还有8桌
您好,客户21!取号成功,您的号码是27,当前叫号是17,前面还有9桌
服务员正在收桌...
请18号用户就餐!
您好,客户21!您的号码是27,当前叫号是18,前面还有8桌
您好,客户26!您的号码是26,当前叫号是18,前面还有7桌
您好,客户18!您的号码是25,当前叫号是18,前面还有6桌
您好,客户10!您的号码是24,当前叫号是18,前面还有5桌
您好,客户5!您的号码是23,当前叫号是18,前面还有4桌
您好,客户2!您的号码是22,当前叫号是18,前面还有3桌
您好,客户29!您的号码是21,当前叫号是18,前面还有2桌
您好,客户20!您的号码是20,当前叫号是18,前面还有1桌
您好,客户8!您的号码是19,当前叫号是18,前面还有0桌
您好,客户3!取号成功,您的号码是28,当前叫号是18,前面还有9桌
您好,客户6!取号成功,您的号码是29,当前叫号是18,前面还有10桌
您好,客户23!取号成功,您的号码是30,当前叫号是18,前面还有11桌
服务员正在收桌...
请19号用户就餐!
您好,客户23!您的号码是30,当前叫号是19,前面还有10桌
您好,客户6!您的号码是29,当前叫号是19,前面还有9桌
您好,客户3!您的号码是28,当前叫号是19,前面还有8桌
您好,客户20!您的号码是20,当前叫号是19,前面还有0桌
您好,客户29!您的号码是21,当前叫号是19,前面还有1桌
您好,客户2!您的号码是22,当前叫号是19,前面还有2桌
您好,客户5!您的号码是23,当前叫号是19,前面还有3桌
您好,客户10!您的号码是24,当前叫号是19,前面还有4桌
您好,客户18!您的号码是25,当前叫号是19,前面还有5桌
您好,客户26!您的号码是26,当前叫号是19,前面还有6桌
您好,客户21!您的号码是27,当前叫号是19,前面还有7桌
服务员正在收桌...
请20号用户就餐!
您好,客户21!您的号码是27,当前叫号是20,前面还有6桌
您好,客户26!您的号码是26,当前叫号是20,前面还有5桌
您好,客户18!您的号码是25,当前叫号是20,前面还有4桌
您好,客户10!您的号码是24,当前叫号是20,前面还有3桌
您好,客户5!您的号码是23,当前叫号是20,前面还有2桌
您好,客户2!您的号码是22,当前叫号是20,前面还有1桌
您好,客户29!您的号码是21,当前叫号是20,前面还有0桌
您好,客户3!您的号码是28,当前叫号是20,前面还有7桌
您好,客户6!您的号码是29,当前叫号是20,前面还有8桌
您好,客户23!您的号码是30,当前叫号是20,前面还有9桌
服务员正在收桌...
请21号用户就餐!
您好,客户23!您的号码是30,当前叫号是21,前面还有8桌
您好,客户6!您的号码是29,当前叫号是21,前面还有7桌
您好,客户3!您的号码是28,当前叫号是21,前面还有6桌
您好,客户2!您的号码是22,当前叫号是21,前面还有0桌
您好,客户5!您的号码是23,当前叫号是21,前面还有1桌
您好,客户10!您的号码是24,当前叫号是21,前面还有2桌
您好,客户18!您的号码是25,当前叫号是21,前面还有3桌
您好,客户26!您的号码是26,当前叫号是21,前面还有4桌
您好,客户21!您的号码是27,当前叫号是21,前面还有5桌
服务员正在收桌...
请22号用户就餐!
您好,客户21!您的号码是27,当前叫号是22,前面还有4桌
您好,客户26!您的号码是26,当前叫号是22,前面还有3桌
您好,客户18!您的号码是25,当前叫号是22,前面还有2桌
您好,客户10!您的号码是24,当前叫号是22,前面还有1桌
您好,客户5!您的号码是23,当前叫号是22,前面还有0桌
您好,客户3!您的号码是28,当前叫号是22,前面还有5桌
您好,客户6!您的号码是29,当前叫号是22,前面还有6桌
您好,客户23!您的号码是30,当前叫号是22,前面还有7桌
服务员正在收桌...
请23号用户就餐!
您好,客户23!您的号码是30,当前叫号是23,前面还有6桌
您好,客户6!您的号码是29,当前叫号是23,前面还有5桌
您好,客户3!您的号码是28,当前叫号是23,前面还有4桌
您好,客户10!您的号码是24,当前叫号是23,前面还有0桌
您好,客户18!您的号码是25,当前叫号是23,前面还有1桌
您好,客户26!您的号码是26,当前叫号是23,前面还有2桌
您好,客户21!您的号码是27,当前叫号是23,前面还有3桌
服务员正在收桌...
请24号用户就餐!
您好,客户21!您的号码是27,当前叫号是24,前面还有2桌
您好,客户26!您的号码是26,当前叫号是24,前面还有1桌
您好,客户18!您的号码是25,当前叫号是24,前面还有0桌
您好,客户3!您的号码是28,当前叫号是24,前面还有3桌
您好,客户6!您的号码是29,当前叫号是24,前面还有4桌
您好,客户23!您的号码是30,当前叫号是24,前面还有5桌
服务员正在收桌...
请25号用户就餐!
您好,客户23!您的号码是30,当前叫号是25,前面还有4桌
您好,客户6!您的号码是29,当前叫号是25,前面还有3桌
您好,客户3!您的号码是28,当前叫号是25,前面还有2桌
您好,客户26!您的号码是26,当前叫号是25,前面还有0桌
您好,客户21!您的号码是27,当前叫号是25,前面还有1桌
服务员正在收桌...
请26号用户就餐!
您好,客户21!您的号码是27,当前叫号是26,前面还有0桌
您好,客户3!您的号码是28,当前叫号是26,前面还有1桌
您好,客户6!您的号码是29,当前叫号是26,前面还有2桌
您好,客户23!您的号码是30,当前叫号是26,前面还有3桌
服务员正在收桌...
请27号用户就餐!
您好,客户23!您的号码是30,当前叫号是27,前面还有2桌
您好,客户6!您的号码是29,当前叫号是27,前面还有1桌
您好,客户3!您的号码是28,当前叫号是27,前面还有0桌
服务员正在收桌...
请28号用户就餐!
您好,客户6!您的号码是29,当前叫号是28,前面还有0桌
您好,客户23!您的号码是30,当前叫号是28,前面还有1桌
服务员正在收桌...
请29号用户就餐!
您好,客户23!您的号码是30,当前叫号是29,前面还有0桌
服务员正在收桌...
请30号用户就餐!
服务员正在收桌...
服务员正在收桌...
服务员正在收桌...
服务员正在收桌...
服务员正在收桌...
服务员正在收桌...
服务员正在收桌...
服务员正在收桌...
服务员正在收桌...
服务员正在收桌...

每一次的执行结果大概率上都不相同。这次的案例简单模拟了餐厅排队系统的运行过程。这里的代码我就不带大家去阅读,代码本身难度不大,相信大家都可以看得明白。

接下来我来总结一下wait、notify、notifyAll这三个方法。

1、从代码上看,wait、notify、notifyAll这三个方法的使用前必须加锁,配合synchronized使用。

2、synchronized锁对于代码中的每个线程来说必须是同一把锁。

3、当执行线程run方法中的wait方法,线程进入等待中,锁释放。当某个线程在run方法中执行到nofity或nofityAll方法会唤醒正在等待的其他线程。正在等待的线程在等待的地方(wait方法所在处)继续往下执行。

4、执行wait方法的线程除了可以被其他线程唤醒,还可以自我唤醒,其实就是超时机制。代码中模拟用户等待时间过长时获取进度(wait超时机制)wait((new Random().nextInt(60) + 1) * 1000),就是使用超时机制。

5、使用nofity只能唤醒一个正在等待的线程,使用nofityAll能唤醒所有正在等待的线程。我们应该尽量使用nofityAll,避免遗漏需要唤醒的线程。

6、wait、notify、notifyAll适合实现类似等待通知的情形。格式:

等待情形:

1)获取同一把锁

2)循环条件判断,不满足条件则调用wait方法进行等待

3)条件满足,往下执行其他业务

通知情形:

1)获取同一把锁

2)改变条件

3)通知在等待的线程