上一篇主要介绍了线程间通信的基本方式——等待/通知机制,并且详细分析了wait()和notify()方法、线程对象的几种状态以及线程在这几种状态之间切换的条件。本篇主要介绍使用等待/通知机制实现生产者/消费者模式的几种形式,以及其他的一些实际运用。
一、生产者/消费者模式
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模式的过程中,如果先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式。
转自:http://ifeve.com/producers-and-consumers-mode/
二、生产者/消费者模式的实现
一生产与一消费:操作值
等待/通知模式最经典的案例就是生产者/消费者模式。但在此模式的基础上还有很多种的变化,不过基本原理都是基于wait/notify的。在只有一个生产者和一个消费者的情况下,生产者和消费者交替运行,生产者每次生产一个产品,然后通知消费者消费;消费者每次消费一个产品,然后通知生产者执行生产任务。假设产品的仓库中只能存放一件产品,则当产品数为1时,生产者线程wait;产品数为0时,消费者线程wait。
生产者与消费者类:
public class Produce { private Object lock; public Produce(Object lock) { super(); this.lock=lock; } public void pro() { try { synchronized(lock) { if(ProductNumber.num==1) lock.wait(); ProductNumber.num=1; System.out.println("生产产品,产品剩余"+ProductNumber.num+"个"); lock.notify(); } }catch(InterruptedException e) { e.printStackTrace(); } } }
public class Consume { private Object lock; public Consume(Object lock) { super(); this.lock=lock; } public void con() { try { synchronized(lock) { if(ProductNumber.num==0) lock.wait(); ProductNumber.num=0; System.out.println("消费产品,产品剩余"+ProductNumber.num+"个"); lock.notify(); } }catch(InterruptedException e) { e.printStackTrace(); } } }
相应的线程:
public class ThP extends Thread{ private Produce p; public ThP(Produce p) { super(); this.p=p; } @Override public void run() { while(true) { p.pro(); } } } public class ThC extends Thread{ private Consume c; public ThC(Consume c) { super(); this.c=c; } @Override public void run() { while(true) { c.con(); } } }
主函数:
public static void main(String args[]) { Object lock = new Object(); Produce p = new Produce(lock); Consume c = new Consume(lock); ThP thp = new ThP(p); ThC thc = new ThC(c); thp.setName("生产者"); thc.setName("消费者"); thp.start(); thc.start(); }
执行后结果如下,生产者和消费者交替执行:
/*
生产产品,产品剩余1个
消费产品,产品剩余0个
生产产品,产品剩余1个
消费产品,产品剩余0个
生产产品,产品剩余1个
消费产品,产品剩余0个
生产产品,产品剩余1个
消费产品,产品剩余0个
生产产品,产品剩余1个
消费产品,产品剩余0个
*/
多生产与多消费:操作值-假死
“假死”现象其实就是全部线程都进入Waiting等待状态,如果全部线程都进入等待状态,那么程序就不执行任何任务了。这在使用生产者消费者模式时经常遇到。
仍旧是上述情况,如果我们把生产者和消费者进程各自创建两个,那么当一个生产者执行完生产任务后,调用notify方法唤醒线程,这时,极有可能它唤醒的是另一个生产者线程。而被唤醒的生产者线程发现仓库中有产品,所以执行wait()方法进入等待,而此时,它没有唤醒任何线程,而在就绪队列中又不存在任何可以竞争cpu资源的就绪线程,这个时候,所有的线程都变成了等待状态,这就是“假死”现象。
解决这个问题很简单,notify()可能连续唤醒同类线程,将notify()改为notifyAll()唤醒所有线程,则可以解决“假死”问题。在一个生产者线程执行完生产任务之后,调用notifyAll()方法,将所有的wait状态的线程唤醒,使它们加入到就绪队列中去。这时,就算另一个生产者线程获得了cpu资源,执行wait,剩下的消费者线程依旧会继续竞争cpu资源,从而杜绝了所有线程都进入Waiting状态的“假死”现象。
多生产多与消费:操作栈
生产者与消费者模式对于集合的操作与针对值的操作有一些不同,不过在原理上基本一致。对于集合操作时,生产者负责向集合中放入数据,而消费者负责将集合中的数据取出并进行相对应的处理。这里我们来举一个例子,由3个生产者和3个消费者共同工作,他们之间有一个数据缓冲区。当缓冲区中的元素个数小于5时,生产者轮番向缓冲区中加入元素,每次生产一个;当缓冲区中还有元素时,消费者轮番从缓冲区中取出元素。每次执行完生产/消费的任务后,程序执行notifyAll()方法唤醒所有线程。
我们假设有三个兵工厂一起生产Kar98k狙击步枪,每次生产一把放入兵器库(缓冲区);三个士兵轮番从兵器库中运出枪支,每次只能拿走一把;兵器库中的最大容量是5把:
//缓冲区 public class MyList { public static List myList = new ArrayList<>(); } //生产者类 public class Pro { private Object lock; public Pro(Object lock) { super(); this.lock=lock; } public void add() { try{ synchronized(lock) { while(MyList.myList.size()==5) lock.wait(); System.out.println(Thread.currentThread().getName()+"执行武器生产任务..."); MyList.myList.add("Kar98k狙击步枪"); System.out.println("仓库中现在有"+MyList.myList.size()+"把Kar98k狙击步枪"); lock.notifyAll(); Thread.sleep(1000); } }catch(InterruptedException e) { e.printStackTrace(); } } } //消费者类 public class Con { private Object lock; public Con(Object lock) { super(); this.lock=lock; } public void get() { try{ synchronized(lock) { while(MyList.myList.size()==0) lock.wait(); System.out.println(Thread.currentThread().getName()+"将武器送上前线..."); MyList.myList.remove(0); System.out.println("仓库中现在有"+MyList.myList.size()+"把Kar98k狙击步枪"); lock.notifyAll(); Thread.sleep(1000); lock.wait(); } }catch(InterruptedException e) { e.printStackTrace(); } } } //生产者线程 public class ThP extends Thread{ private Pro p; public ThP(Pro p) { super(); this.p = p; } @Override public void run() { while(true) { try { p.add(); Thread.sleep(10); }catch(InterruptedException e) { e.printStackTrace(); } } } } //消费者线程 public class ThC extends Thread{ private Con c; public ThC(Con c) { super(); this.c = c; } @Override public void run() { while(true) { try { c.get(); Thread.sleep(10); }catch(InterruptedException e) { e.printStackTrace(); } } } } //主函数 public class Run { public static void main(String args[]) { Object lock = new Object(); for(int i=0;i<3;i++) { Pro p = new Pro(lock); Con c = new Con(lock); ThP thp = new ThP(p); ThC thc = new ThC(c); thp.setName("兵工厂"+(i+1)); thc.setName("士兵"+(i+1)); thp.start(); thc.start(); } } }
为了避免一个线程长时间占用cpu执行,同时为了便于观察多生产者和多消费者共同工作,我们在生产者线程和消费者线程内调用相应方法后,使用sleep使当前线程短暂休眠。
部分运行结果如下:
/*
仓库中现在有4把Kar98k狙击步枪
兵工厂3执行武器生产任务...
仓库中现在有5把Kar98k狙击步枪
士兵3将武器送上前线...
仓库中现在有4把Kar98k狙击步枪
士兵1将武器送上前线...
仓库中现在有3把Kar98k狙击步枪
士兵3将武器送上前线...
仓库中现在有2把Kar98k狙击步枪
兵工厂1执行武器生产任务...
仓库中现在有3把Kar98k狙击步枪
兵工厂2执行武器生产任务...
仓库中现在有4把Kar98k狙击步枪
兵工厂2执行武器生产任务...
*/可以看到的是,在程序的执行过程中,每当一个工厂生产一把枪后,会调用notifyAll(),然后执行完同步监视器内的代码之后释放lock对象锁,由剩下的生产者、消费者线程共同竞争对象锁。每当仓库中有五把枪时,所有生产者线程都会被wait阻塞;当仓库中没有枪时,所有消费者会wait阻塞。