java生产者消费者实现及优化
生产者消费者模型是多线程中经常遇到的编程模型。java中可以通过wait notify notifyAll来实现生产者消费者模型,但是并发程序的正确编写需要遵守一些准则,否则程序便会出现各种问题,如下一种错误实现:
Consume.java:
public class Consume implements Runnable{ private List container = null; private int count; public Consume(List lst) { this.container = lst; } public void run() { // while(true) { synchronized (container) { if(container.size()== 0){ try { container.wait();//容器为空,放弃锁,等待生产 } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我准备吃"+(++MultiThread.consume_count)+"个"); container.remove(0); container.notify(); } } // } }
Product.java:
public class Product implements Runnable { private List container = null; public Product(List lst) { this.container = lst; } public void run() { // while (true) { synchronized (container) { System.out.println("enter produce "); if (container.size() >= MultiThread.MAX) { //如果容器超过了最大值,就不要在生产了,等待消费 try { container.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } container.add(new Object()); container.notify(); System.out.println("我生产了"+(++MultiThread.pro_count)+"个"); } } // } }
MultiThread.java:
public class MultiThread { private List container = new ArrayList(); public final static int MAX = 2; public static int pro_count = 0; public static int consume_count = 0; public static void main(String args[]) { MultiThread m = new MultiThread(); new Thread(new Consume(m.getContainer())).start(); new Thread(new Consume(m.getContainer())).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new Product(m.getContainer())).start(); } public List getContainer() { return container; } public void setContainer(List container) { this.container = container; } }
运行结果:
enter produce 我生产了1个 我准备吃1个 我准备吃2个 Exception in thread "Thread-1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:635) at java.util.ArrayList.remove(ArrayList.java:474) at Consume.run(Consume.java:29) at java.lang.Thread.run(Thread.java:745) Process finished with exit code 0
程序运行时,消费者在没有资源的时候继续消费,导致Consume.java:29行处,即container.remove(0)处抛出异常。
那么为什么程序会抛出异常?
程序先启动了两个消费者线程,由于一开始并没有资源,所以两个消费者都在wait处阻塞挂起。等待notify的唤醒。
new Thread(new Consume(m.getContainer())).start(); new Thread(new Consume(m.getContainer())).start();
随后程序创建了一个生产者线程
new Thread(new Product(m.getContainer())).start();
此时,生产者生产了一个资源后,调用生产者的notify。注意这里不是notifyAll。按理应该只唤醒之前两个消费者中的一个,但是执行后居然两个消费者都醒了。一个生产者只生产了一个资源,所以当两个消费者都执行的话导致了对空链表的删除,所以导致程序抛出异常。
那么为什么这里执行了一个生产者,两个消费者都被唤醒了呢?
其实,生产者确实只唤醒了一个消费者,最先被唤醒的消费者执行后,自身又会调用nofify,从而将第二个挂起的消费者唤醒(对,这里是消费者把消费者唤醒了!!)。
如何解决这个问题?
我们应该在调用wait之前测试条件谓词,并且从wait中返回时再次测试条件谓词。即将wait放在一个循环中。
但是这样还是存在问题,因为第一个被唤醒的消费者的notify被第二个消费者捕获了,而第二个消费者唤醒后经过循环判断,发现这个唤醒是没用的,所以重新挂起,最终这个notify将丢失,本因被唤醒的成产者可能一直都无法唤醒。在更复杂的系统中,可能导致意想不到的的错误。所以,我们应将notify改成notifyAll。
最终正确的代码如下:
Consume.java
public class Consume implements Runnable{ private List container = null; public Consume(List lst) { this.container = lst; } public void run() { // while(true) { synchronized (container) { while(container.size()== 0){ try { container.wait();//容器为空,放弃锁,等待生产 } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我准备吃"+(++MultiThread.consume_count)+"个"); container.remove(0); container.notifyAll(); } } // } }
Produce.java
public class Product implements Runnable { private List container = null; public Product(List lst) { this.container = lst; } public void run() { // while (true) { synchronized (container) { while (container.size() >= MultiThread.MAX) { //如果容器超过了最大值,就不要在生产了,等待消费 try { container.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } container.add(new Object()); container.notifyAll(); System.out.println("我生产了"+(++MultiThread.pro_count)+"个"); } } // } }
对于类似的需求,我们可以套用一个模板来防止程序出现错误:
void staeDependentMethod() throws InterruptedException { //必须通过一个锁来保护条件谓语 synchronized (lock) { while(!conditionPredicate()) { lock.wait(); } lock.notifyAll(); } }
然而,这里又存在一个性能的问题。
因为notifyAll比notify更低效,这种低效情况带来的影响,有时候很小,但有时候却很大。如果N个线程在等待,那么调用notifyAll将唤醒每个线程,并使它们在锁上发生竞争。然后,它们中的大部分或者全部又都回到休眠状态。因而,在每个线程执行一个事件的同时,将出现大量的上下文切换操作以及发生竞争的锁获取操作。
所以,我们需要对上面的程序进行优化,减少不必要的调用。其实,只需要在从空变为非空,或者从满变为非满时,才需要释放一个线程。
boolean isEmpty = container.isEmpty(); container.add(new Object()); if(isEmpty) container.notifyAll();
修改后的程序如下所示
Consume.java
public class Consume implements Runnable{ private List container = null; public Consume(List lst) { this.container = lst; } public void run() { while(true) { synchronized (container) { while(container.size()== 0){ try { container.wait();//容器为空,放弃锁,等待生产 } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我准备吃"+(++MultiThread.consume_count)+"个"); boolean isFull = (container.size() == MultiThread.MAX); container.remove(0); if(isFull) container.notifyAll(); } } } }
Product.java
public class Product implements Runnable { private List container = null; public Product(List lst) { this.container = lst; } public void run() { while (true) { synchronized (container) { while (container.size() >= MultiThread.MAX) { //如果容器超过了最大值,就不要在生产了,等待消费 try { container.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } boolean isEmpty = container.isEmpty(); container.add(new Object()); if(isEmpty) container.notifyAll(); System.out.println("我生产了"+(++MultiThread.pro_count)+"个"); } } } }
当然,程序还是可以再优化的,使得发生更少的上下文切换。
上面的程序使用的是内置条件队列,但是每个内置锁都只能有一个相关联的条件队列,多个线程在一个条件队列上等待不同的条件谓词时无法做到定向唤醒。针对这个缺陷,我们可以使用显示的Lock和Condition(一个Lock可以任意数量的Condition对象)。代码如下:
Consume.java
public class Consume implements Runnable{ private List container = null; private Lock lock = null; private Condition notFull = null; private Condition notEmpty = null; public Consume(List lst, Lock lk, Condition nf, Condition ne) { this.container = lst; this.lock = lk; this.notFull = nf; this.notEmpty = ne; } public void run() { while(true) { lock.lock(); try { while(container.size()== 0){ try { notEmpty.await();//容器为空,放弃锁,等待生产 } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我准备吃"+(++MultiThread.consume_count)+"个"); container.remove(0); notFull.signal(); } finally { lock.unlock(); } } } }
Product.java
public class Product implements Runnable { private List container = null; private Lock lock = null; private Condition notFull = null; private Condition notEmpty = null; public Product(List lst, Lock lk, Condition nf, Condition ne) { this.container = lst; this.lock = lk; this.notFull = nf; this.notEmpty = ne; } public void run() { while (true) { lock.lock(); try { while (container.size() >= MultiThread.MAX) { //如果容器超过了最大值,就不要在生产了,等待消费 try { notFull.await(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } container.add(new Object()); System.out.println("我生产了"+(++MultiThread.pro_count)+"个"); notEmpty.signal(); } finally { lock.unlock(); } } } }
一个Condition和一个Lock关联在一起,就像一个条件队列和一个内置锁相关联一样。Condition的存在,使得每个锁上可存在多个条件队列操作,而且可以定向唤醒。这样可以极大减少在操作中发生的上下文切换和请求次数。