java并发编程系列:生产者-消费者模式

目光从厕所转到饭馆,一个饭馆里通常都有好多厨师以及好多服务员,这里我们把厨师称为生产者,把服务员称为消费者,厨师和服务员是不直接打交道的,而是在厨师做好菜之后放到窗口,服务员从窗口直接把菜端走给客人就好了,这样会极大的提升工作效率,因为省去了生产者和消费者之间的沟通成本。从java的角度看这个事情,每一个厨师就相当于一个生产者线程,每一个服务员都相当于一个消费者线程,而放菜的窗口就相当于一个缓冲队列,生产者线程不断把生产好的东西放到缓冲队列里,消费者线程不断从缓冲队列里取东西,画个图就像是这样:


现实中放菜的窗口能放的菜数量是有限的,我们假设这个窗口只能放5个菜。那么厨师在做完菜之后需要看一下窗口是不是满了,如果窗口已经满了的话,就在一旁抽根烟等待,直到有服务员来取菜的时候通知一下厨师窗口有了空闲,可以放菜了,这时厨师再把自己做的菜放到窗口上去炒下一个菜。从服务员的角度来说,如果窗口是空的,那么也去一旁抽根烟等待,直到有厨师把菜做好了放到窗口上,并且通知他们一下,然后再把菜端走。
我们先用java抽象一下菜:

public class Food {
    private static int counter = 0;

    private int i;  //代表生产的第几个菜

    public Food() {
        i = ++counter;
    }

    @Override
    public String toString() {
        return "第" + i + "个菜";
    }
}

每次创建Food对象,字段i的值都会加1,代表这是创建的第几道菜。

为了故事的顺利进行,我们首先定义一个工具类:

class SleepUtil {

    private static Random random = new Random();
    
    public static void randomSleep() {
        try {
            Thread.sleep(random.nextInt(1000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

SleepUtil的静态方法randomSleep代表当前线程随机休眠一秒内的时间。

然后我们再用java定义一下厨师:

public class Cook extends Thread {

    private Queue<Food> queue;

    public Cook(Queue<Food> queue, String name) {
        super(name);
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            SleepUtil.randomSleep();    //模拟厨师炒菜时间
            Food food = new Food();
            System.out.println(getName() + " 生产了" + food);
            synchronized (queue) {
                while (queue.size() > 4) {
                    try {
                        System.out.println("队列元素超过5个,为:" + queue.size() + " " + getName() + "抽根烟等待中");
                        queue.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                queue.add(food);
                queue.notifyAll();
            }
        }
    }
}

我们说每一个厨师Cook都是一个线程,内部维护了一个名叫queue的队列。在run方法中是一个死循环,代表不断的生产Food。他每生产一个Food后,都要判断queue队列中元素的个数是不是大于4,如果大于4的话,就调用queue.wait()等待,如果不大于4的话,就把创建号的Food对象放到queue队列中,由于可能多个线程同时访问queue的各个方法,所以对这段代码用queue对象来加锁保护。当向队列添加完刚创建的Food对象之后,就可以通知queue这个锁对象关联的等待队列中的服务员线程们可以继续端菜了。

然后我们再用java定义一下服务员:

class Waiter extends Thread {

    private Queue<Food> queue;

    public Waiter(Queue<Food> queue, String name) {
        super(name);
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            Food food;
            synchronized (queue) {
                while (queue.size() < 1) {
                    try {
                        System.out.println("队列元素个数为: " + queue.size() + "," + getName() + "抽根烟等待中");
                        queue.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                food = queue.remove();
                System.out.println(getName() + " 获取到:" + food);
                queue.notifyAll();
            }

            SleepUtil.randomSleep();    //模拟服务员端菜时间
        }
    }
}

每个服务员也是一个线程,和厨师一样,都在内部维护了一个名叫queue的队列。在run方法中是一个死循环,代表不断的从队列中取走Food。每次在从queue队列中取Food对象的时候,都需要判断一下队列中的元素是否小于1,如果小于1的话,就调用queue.wait()等待,如果不小于1的话,也就是队列里有元素,就从队列里取走一个Food对象,并且通知与queue这个锁对象关联的等待队列中的厨师线程们可以继续向队列里放入Food对象了。
在厨师和服务员线程类都定义好了之后,我们再创建一个Restaurant类,来看看在餐馆里真实发生的事情:

public class Restaurant {

    public static void main(String[] args) {

        Queue<Food> queue = new LinkedList<>();
        new Cook(queue, "1号厨师").start();
        new Cook(queue, "2号厨师").start();
        new Cook(queue, "3号厨师").start();
        new Waiter(queue, "1号服务员").start();
        new Waiter(queue, "2号服务员").start();
        new Waiter(queue, "3号服务员").start();
    }
}

我们在Restaurant中安排了3个厨师和3个服务员,大家执行一下这个程序,会发现在如果厨师生产的过快,厨师就会等待,如果服务员端菜速度过快,服务员就会等待。但是整个过程厨师和服务员是没有任何关系的,它们是通过队列queue实现了所谓的解耦。

这个过程虽然不是很复杂,但是使用中还是需要注意一些问题:

  • 我们这里的厨师和服务员使用同一个锁queue。

使用同一个锁是因为对queue的操作只能用同一个锁来保护,假设使用不同的锁,厨师线程调用queue.add方法,服务员线程调用queue.remove方法,这两个方法都不是原子操作,多线程并发执行的时候会出现不可预测的结果,所以我们使用同一个锁来保护对queue这个变量的操作,这一点我们在唠叨设计线程安全类的时候已经强调过了。

  • 厨师和服务员线程使用同一个锁queue的后果就是厨师线程和服务员线程使用的是同一个等待队列。

但是同一时刻厨师线程和服务员线程不会同时在等待队列中,因为当厨师线程在wait的时候,队列里的元素肯定是5,此时服务员线程肯定是不会wait的,但是消费的过程是被锁对象queue保护的,所以在一个服务员线程消费了一个Food之后,就会调用notifyAll来唤醒等待队列中的厨师线程们;当消费者线程在wait的时候,队列里的元素肯定是0,此时厨师线程肯定是不会wait的,生产的过程是被锁对象queue保护的,所以在一个厨师线程生产了一个Food对象之后,就会调用notifyAll来唤醒等待队列中的服务员线程们。所以同一时刻厨师线程和服务员线程不会同时在等待队列中。

  • 在生产和消费过程,我们都调用了SleepUtil.randomSleep();。

我们这里的生产者-消费者模型是把实际使用的场景进行了简化,真正的实际场景中生产过程和消费过程一般都会很耗时,这些耗时的操作最好不要放在同步代码块中,这样会造成别的线程的长时间阻塞。如果把生产过程和消费过程都放在同步代码块中,也就是说在一个厨师炒菜的同时不允许别的厨师炒菜,在一个服务员端菜的同时不允许别的服务员端菜,这个显然是不合理的,大家需要注意这一点。

以上就是wait/notify机制的一个现实应用:生产者-消费者模式的一个简介。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容