前言
生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒
示意图如下
![8af3baa3ff976c8068f61ea891f09242.png](https://i-blog.csdnimg.cn/blog_migrate/369d8de3403bb4a3cfc48b71f14c8949.jpeg)
如何解决这个问题
1.既然是消费者和生产者同时访问该内存,那么肯定是多线程
2.消费者和生产者拿到的肯定是同一种商品,那么就要采用某种机制保护生产者和消费者之间的同步
保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。
这里我们用多种方法来实现,首先是最常见的wait/notify
1.wait() / notify()方法
思路:
以我们去店里买包子为例
当店家蒸好包子了,就不会再蒸,而是进行吆喝,通知消费者来购买
当消费者把包子买完了,就不会在购买,而是在店里等待,通知店家再蒸一笼
换成多线程即:
当生产者生产商品满时,停止执行,放弃锁进入等待,并通知消费者进行消费
当消费者消费商品完时,停止执行,放弃锁进入等待,并通知生产者进行生产
代码如下:
商品类(包子)
![78fb8038aa1b35cae1fc241085a5a0a3.png](https://i-blog.csdnimg.cn/blog_migrate/c94bb0c3a2a8966f1adaf41782cece16.jpeg)
生产者(包子铺)
![745e612653cfea653f1d0bbf0b14206b.png](https://i-blog.csdnimg.cn/blog_migrate/ebb7d1152723265bad35c1cf5dc112df.jpeg)
消费者(吃货)
![2b77cdb6f22cdd3286c03d99cb63342a.png](https://i-blog.csdnimg.cn/blog_migrate/2d168c1a348d49f57e62404763ab9d4b.jpeg)
主函数
![96ceddd619d92dda000531bad592127a.png](https://i-blog.csdnimg.cn/blog_migrate/59c6e52be905ecec3b74ebd260e170b6.jpeg)
运行结果
![526a952a0d1baa63579a62e84348d733.png](https://i-blog.csdnimg.cn/blog_migrate/ab7afb6d79c3199d659156eca0a384ff.jpeg)
本次测试中,有3个吃货,3个包子铺,包子铺生产一个包子,休眠1s,吃货吃一个包子,休息3s,结果显示包子铺生产速度明显快于吃货吃包子速度,符合预想
2. BlockingQueue阻塞队列方法
BlockingQueue是1.5新增的内容,是一个同步队列,它在内部实现了线程同步,实现方式采用await() / signal()方法。它可以在生成对象时指定容量大小,用于阻塞操作的是put()和take()方法。
put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞
![3834a9a439352f11ac53ab328f6ac293.png](https://i-blog.csdnimg.cn/blog_migrate/b13437d49b6917a57b024b9f0b42b66e.jpeg)
运行结果:
![fd9db315fe542ebeb1e0e7cf19d2ffdf.png](https://i-blog.csdnimg.cn/blog_migrate/fc01f903fbcf18536afeec2c8db92749.jpeg)
结果发现System.out.println()输出明显有问题,会多次输出相同值,但整体来看总量确没有问题,那是因为put()或take()对list来说是同步的,但System.out.println()输出并不同步,当两个线程put()成功时,这时list.size() = 2;此时两个线程输出结果都为2。
3.await() / signal()方法
上面我们说过BlockingQueue底层是实现await() / signal()方法,下面我们来看一下
在JDK1.5中,用ReentrantLock和Condition可以实现等待/通知模型,具有更大的灵活性。通过在Lock对象上调用newCondition()方法,将条件变量和一个锁对象进行绑定,进而控制并发程序访问竞争资源的安全
![c232fbc23c482aa3c76bdff26a3f8579.png](https://i-blog.csdnimg.cn/blog_migrate/b0bb1d16f64644773ea4947f916596b9.jpeg)
运行结果与wait()/notify()类似
4. 信号量
Semaphore是一个计数的信号量。设置一个阈值,多个线程竞争获取信号,执行完毕后归还,当超过设定值时,线程申请许可信号被阻塞,常用来构造资源池如数据库连接池,对象池等。当阈值为1时,因只有两个状态,也叫二元信号量,可作为互斥锁使用。
![6e7135da5e48f5e17ece10683361254f.png](https://i-blog.csdnimg.cn/blog_migrate/3d8fd43d731b225c0102f202bdf4a104.jpeg)
5. 管道
一种特殊的流,用于不同线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输入管道中读数据。
inputStream.connect(outputStream)或outputStream.connect(inputStream)作用是使两个Stream之间产生通信链接,这样才可以将数据进行输出与输入。
这种方式只适用于两个线程之间通信,不适合多个线程之间通信。
PipedInputStream / PipedOutputStream (操作字节流)
Producer
![0e3a95f37ab183a71a4f4e4bc3b41ffd.png](https://i-blog.csdnimg.cn/blog_migrate/730c40620e56039115721682b58615d5.jpeg)
consumer
![b6401492426cd63ea4c1d782dc78d3b4.png](https://i-blog.csdnimg.cn/blog_migrate/82bce92ce966f6724c21beaa0ffc0bd8.jpeg)
Main
![50f5ded0f5826ea8a3bb5ea939475baf.png](https://i-blog.csdnimg.cn/blog_migrate/abcd88f609ccf315b2c77834ffd71977.jpeg)
PipedReader / PipedWriter (操作字符流)与以上字节流操作一致,代码就不放了
以上就是生产者消费者问题线程同步常见的几种方式。