前言
生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒
示意图如下
![f65a2ae45ac48e153b50c3eb8bae47be.png](https://img-blog.csdnimg.cn/img_convert/f65a2ae45ac48e153b50c3eb8bae47be.png)
如何解决这个问题
1.既然是消费者和生产者同时访问该内存,那么肯定是多线程
2.消费者和生产者拿到的肯定是同一种商品,那么就要采用某种机制保护生产者和消费者之间的同步
保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。
这里我们用多种方法来实现,首先是最常见的wait/notify
1.wait() / notify()方法
思路:
以我们去店里买包子为例
当店家蒸好包子了,就不会再蒸,而是进行吆喝,通知消费者来购买
当消费者把包子买完了,就不会在购买,而是在店里等待,通知店家再蒸一笼
换成多线程即:
当生产者生产商品满时,停止执行,放弃锁进入等待,并通知消费者进行消费
当消费者消费商品完时,停止执行,放弃锁进入等待,并通知生产者进行生产
代码如下:
商品类(包子)
![c96a04f6031e2ec4eefc6f14b6a86f74.png](https://img-blog.csdnimg.cn/img_convert/c96a04f6031e2ec4eefc6f14b6a86f74.png)
生产者(包子铺)
![ca61faac4e458905423ba7d170b466ce.png](https://img-blog.csdnimg.cn/img_convert/ca61faac4e458905423ba7d170b466ce.png)
消费者(吃货)
![1c2293243820d33e69d323ed6dd7777f.png](https://img-blog.csdnimg.cn/img_convert/1c2293243820d33e69d323ed6dd7777f.png)
主函数
![5cf336135badd3c321e7c4baa0488d0c.png](https://img-blog.csdnimg.cn/img_convert/5cf336135badd3c321e7c4baa0488d0c.png)
运行结果
![7ebb11e86356f5b445269abdfc4b5693.png](https://img-blog.csdnimg.cn/img_convert/7ebb11e86356f5b445269abdfc4b5693.png)
本次测试中,有3个吃货,3个包子铺,包子铺生产一个包子,休眠1s,吃货吃一个包子,休息3s,结果显示包子铺生产速度明显快于吃货吃包子速度,符合预想
2. BlockingQueue阻塞队列方法
BlockingQueue是1.5新增的内容,是一个同步队列,它在内部实现了线程同步,实现方式采用await() / signal()方法。它可以在生成对象时指定容量大小,用于阻塞操作的是put()和take()方法。
put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞
![6ba5de0f75d5f09bfd3e9c01cc84f13e.png](https://img-blog.csdnimg.cn/img_convert/6ba5de0f75d5f09bfd3e9c01cc84f13e.png)
运行结果:
![bf1693c0102d8910a5d77159dcbccd5b.png](https://img-blog.csdnimg.cn/img_convert/bf1693c0102d8910a5d77159dcbccd5b.png)
结果发现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()方法,将条件变量和一个锁对象进行绑定,进而控制并发程序访问竞争资源的安全
![8083b2135dc7500fafdb2ae15513a370.png](https://img-blog.csdnimg.cn/img_convert/8083b2135dc7500fafdb2ae15513a370.png)
运行结果与wait()/notify()类似
4. 信号量
Semaphore是一个计数的信号量。设置一个阈值,多个线程竞争获取信号,执行完毕后归还,当超过设定值时,线程申请许可信号被阻塞,常用来构造资源池如数据库连接池,对象池等。当阈值为1时,因只有两个状态,也叫二元信号量,可作为互斥锁使用。
![fd17ac9c741f75e7d568d4d488d020fa.png](https://img-blog.csdnimg.cn/img_convert/fd17ac9c741f75e7d568d4d488d020fa.png)
5. 管道
一种特殊的流,用于不同线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输入管道中读数据。
inputStream.connect(outputStream)或outputStream.connect(inputStream)作用是使两个Stream之间产生通信链接,这样才可以将数据进行输出与输入。
这种方式只适用于两个线程之间通信,不适合多个线程之间通信。
PipedInputStream / PipedOutputStream (操作字节流)
Producer
![bebe462f64ca3882f8ccffef83b23bd9.png](https://img-blog.csdnimg.cn/img_convert/bebe462f64ca3882f8ccffef83b23bd9.png)
consumer
![b5b4d68cae005164b11e26a07c181edb.png](https://img-blog.csdnimg.cn/img_convert/b5b4d68cae005164b11e26a07c181edb.png)
Main
![3a6cf55731b87f79488ff95000e30742.png](https://img-blog.csdnimg.cn/img_convert/3a6cf55731b87f79488ff95000e30742.png)
PipedReader / PipedWriter (操作字符流)与以上字节流操作一致,代码就不放了
以上就是生产者消费者问题线程同步常见的几种方式。