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