标签: 「咱们都是小青蛙」公众号文章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,表明这是建立的第几道菜。并发
为了故事的顺利进行,咱们首先定义一个工具类:dom
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
表明当前线程随机休眠一秒内的时间。ide
而后咱们再用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
机制的一个现实应用:生产者-消费者
模式的一个简介。
写文章挺累的,有时候你以为阅读挺流畅的,那实际上是背后无数次修改的结果。若是你以为不错请帮忙转发一下,万分感谢~ 这里是个人公众号,里边有更多技术干货,时不时扯一下犊子,欢迎关注:
另外,做者还写了一本MySQL小册:《MySQL是怎样运行的:从根儿上理解MySQL》的连接 。小册的内容主要是从小白的角度出发,用比较通俗的语言讲解关于MySQL进阶的一些核心概念,好比记录、索引、页面、表空间、查询优化、事务和锁等,总共的字数大约是三四十万字,配有上百幅原创插图。主要是想下降普通程序员学习MySQL进阶的难度,让学习曲线更平滑一点~ 有在MySQL进阶方面有疑惑的同窗能够看一下: