在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区(也称之为仓库)处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。示意图如下:
生产者消费者问题是研究多线程程序时绕不开的经典问题之一,它描述是有一块缓冲区作为仓库,生产者可以将产品放入仓库,消费者则可以从仓库中取走产品
解决生产者/消费者问题的方法包括以下几种:
(1)wait() / notify()方法
(2)await() / signal()方法
(3)BlockingQueue阻塞队列方法
本篇先介绍(1)(2)两种方法,第三种方法在下篇文章中介绍。
现在给出(1)(2)两种方法解决生产者/消费者问题的通用模板:
* 1.锁{
* 2. while 条件不满足
* 释放锁 , 等待条件改变
* end while
* 3. 生产或者消费
* 4. 唤醒其他生产者或消费者线程
* 5. 释放锁
* }
(1)wait() / notify()方法
wait() / nofity()方法是基类Object的两个方法,也就意味着所有Java类都会拥有这两个方法,这样,我们就可以为任何对象实现同步机制。
wait()方法:当缓冲区已满/空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等等状态,让其他线程执行。
notify()方法:当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
代码如下:
public class Storage {
private int count=0;//仓库中的资源数量
private int maxCount=8;//仓库能存储的最大资源数量
public synchronized void produce(String name) throws InterruptedException{1.加锁
while(count>=maxCount){//2.条件不满足
this.wait();//释放锁,等待条件改变
}
count++;//3.生产
System.out.println(name+"生产一个资源,仓库储量为"+count);
notifyAll();//4.唤醒其他生产者或消费者线程
}//5.释放锁
public synchronized void consume(String name) throws InterruptedException{1.加锁
while(count<=0){//2.条件不满足
this.wait();//释放锁,等待条件改变
}
count--;//3.消费
System.out.println(name+"消费一个资源,仓库储量为"+count);
notifyAll();//4.唤醒其他生产者或消费者线程
}//5.释放锁
public static void main(String[] args) {
Storage s=new Storage();
Thread producer1=new Thread(new Producer(s,"生产者1号"));
Thread producer2=new Thread(new Producer(s,"生产者2号"));
Thread consumer1=new Thread(new Consumer(s,"消费者1号"));
Thread consumer2=new Thread(new Consumer(s,"消费者2号"));
producer1.start();
producer2.start();
consumer1.start();
consumer2.start();
}
}
class Producer implements Runnable{
private Storage s;
private String name="";//线程名
public Producer(Storage s,String name){
this.s=s;
this.name=name;
}
@Override
public void run() {
while(true){
try {
s.produce(name);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable{
private Storage s;
private String name="";//线程名
public Consumer(Storage s,String name){
this.s=s;
this.name=name;
}
@Override
public void run() {
while(true){
try {
s.consume(name);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
(2)await() / signal()方法
在JDK5.0之后,Java提供了更加健壮的线程处理机制,包括同步、锁定、线程池等,它们可以实现更细粒度的线程控制。await()和signal()就是其中用来做同步的两种方法,它们的功能基本上和wait() / nofity()相同,完全可以取代它们,但是它们和新引入的锁定机制Lock直接挂钩,具有更大的灵活性。通过在Lock对象上调用newCondition()方法,将条件变量和一个锁对象进行绑定,进而控制并发程序访问竞争资源的安全。下面来看代码:
public class Storage {
private int count=0;//仓库中的资源数量
private int maxCount=8;//仓库能存储的最大资源数量
Lock lock=new ReentrantLock();
Condition full=lock.newCondition();//生产者等待队列
Condition empty=lock.newCondition();//消费者等待队列
public void produce(String name) throws InterruptedException{
lock.lock();//1.加锁
while(count>=maxCount){//2.条件不满足
full.await();;//释放锁,等待条件改变
}
count++;//3.生产
System.out.println(name+"生产一个资源,仓库储量为"+count);
full.signalAll();//4.唤醒其他生产者或消费者线程
empty.signalAll();
lock.unlock();//5.释放锁
}
public void consume(String name) throws InterruptedException{
lock.lock();//1.加锁
while(count<=0){//2.条件不满足
empty.await();;//释放锁,等待条件改变
}
count--;//3.消费
System.out.println(name+"消费一个资源,仓库储量为"+count);
full.signalAll();//4.唤醒其他生产者或消费者线程
empty.signalAll();
lock.unlock();//5.释放锁
}//释放锁
public static void main(String[] args) {
Storage s=new Storage();
Thread producer1=new Thread(new Producer(s,"生产者1号"));
Thread producer2=new Thread(new Producer(s,"生产者2号"));
Thread consumer1=new Thread(new Consumer(s,"消费者1号"));
Thread consumer2=new Thread(new Consumer(s,"消费者2号"));
producer1.start();
producer2.start();
consumer1.start();
consumer2.start();
}
}
class Producer implements Runnable{
private Storage s;
private String name="";//线程名
public Producer(Storage s,String name){
this.s=s;
this.name=name;
}
@Override
public void run() {
while(true){
try {
s.produce(name);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable{
private Storage s;
private String name="";//线程名
public Consumer(Storage s,String name){
this.s=s;
this.name=name;
}
@Override
public void run() {
while(true){
try {
s.consume(name);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
上面给出了两种方法解决消费者/生产者模式的代码,下面给出几个最关键的几点:
(1)生产者和消费者使用同一个仓库作为中介。
(2)生产者和消费者使用同一把锁。
(3)生产者只在仓库未满时进行生产,仓库满时生产者进程被阻塞。
(4)消费者只在仓库非空时进行消费,仓库为空时消费者进程被阻塞。
到此为止使用锁机制实现消费者/生产者模式就介绍完了,下一篇将介绍使用阻塞队列实现消费者/生产者模式。