生产者-消费者模型是进程间通信的重要内容之一。其原理十分简单,但自己用语言实现往往会出现很多的问题,下面我们用一系列代码来展现在编码中容易出现的问题以及最优解决方案。
/* 单生产者、单消费者生产烤鸭 */class Resource { private String name; private int count = 1; //计数器,记录有多少只烤鸭被生产及消费 private boolean flag = false; //停止标记 public synchronized void set(String name) //生产烤鸭方法 { while(flag) //如果flag=true,则等待 { try{this.wait();}catch(InterruptedException e){} //使用wait()方法必须要捕捉异常 } this.name = name + count; count++; System.out.println(Thread.currentThread().getName()+"..."+this.name); flag = true; //生产完一个烤鸭就将flag设为true,等待消费者消费烤鸭 notify(); //唤醒消费者 } public synchronized void out() { while(!flag) { try{this.wait();}catch(InterruptedException e){} } System.out.println(Thread.currentThread().getName()+"........."+this.name); flag = false; //消费完一个烤鸭就将flag设为false,等待生产者生产烤鸭 notify(); //唤醒生产者 } } class Producer implements Runnable { Resource r; Producer(Resource r) { this.r = r; } public void run() { while(true) { r.set("kaoya"); //生产烤鸭 } } } class Consumer implements Runnable { Resource r; Consumer(Resource r) { this.r = r; } public void run() { while(true) { r.out(); //消费烤鸭 } } } public class ResourceDemo { public static void main(String[] args) { Resource r = new Resource(); Producer producer = new Producer(r); Consumer consumer = new Consumer(r); Thread t1 = new Thread(producer); Thread t2 = new Thread(producer); t1.start(); t2.start(); } }
以上是单生产者单消费者的代码,我们来看一下运行结果:
然而正如实际情况,饭店的厨房里不可能只有一个厨子,也不可能只有一个顾客,因此只考虑单消费者单生产者是没有意义的。我们再各加入一个消费者和生产者线程。
很显然,运行中出现了死锁,程序卡住,这非常让人费解。
实际上,在加入两个线程时,整个程序的运行过程已经发生了很大的变化。起始flag为false,因此生产者线程t1可以进入同步区,完成后将flag设为true。在这种情况下,生产者线程t3会进入wait状态。与此同时,两个消费者线程t2和t4可能都会因为一开始flag为false进入wait状态。当线程t1完成整个流程准备notify()时,线程池里一共有三个线程:生产者t3,消费者t2、t4。由于notify()的工作机制是随机唤醒一个线程,最不巧的情况,如果它唤醒的是生产者线程t3,那么t3判断flag为true,又会进入wait状态。此时此刻,所有四个线程都会进入wait状态,自然地,发生了线程死锁。
那么如何解决?最简单的方法是将notify()改为notifyAll(),这样每一次都会唤醒所有的线程。然而这种方法实在是有一点浪费资源,因为本来我们只需要唤醒1个或2个线程,而使用notifyAll()则必须将三个线程都唤醒。面试进行到这里,肯定会有一个问题——如何优化?
我们可以使用JDK1.5的特性Lock来处理,在使用它时需要在头部import java.util.concurrent.locks.*;它可以实现仅唤醒消费者线程或生产者线程,赋予了程序员更多的控制权,自然也能提高程序的效率,使得不必唤醒不需要唤醒的线程。具体代码如下:
import java.util.concurrent.locks.*; class Resource { private String name; private int count = 1; private boolean flag = false; Lock lock = new ReentrantLock(); //新建锁对象 Condition producer_con = lock.newCondition(); //新建生产者condition Condition consumer_con = lock.newCondition(); //新建消费者condition public void set(String name) { lock.lock(); //上锁 try { while(flag) { try{producer_con.await();}catch(InterruptedException e){} //注意在这种情况下要将原来的this.wait()改为producer_con.await } this.name = name + count; count++; System.out.println(Thread.currentThread().getName()+"..."+this.name); flag = true; consumer_con.signal(); //唤醒消费者线程 } finally { lock.unlock(); //把解锁放在finally里,不管中间是否有异常一定要解锁 } } public void out() { lock.lock(); try { while(!flag) { try{consumer_con.await();}catch(InterruptedException e){} } System.out.println(Thread.currentThread().getName()+"........."+this.name); flag = false; producer_con.signal(); //唤醒生产者线程 } finally { lock.unlock(); } } } class Producer implements Runnable { Resource r; Producer(Resource r) { this.r = r; } public void run() { while(true) { r.set("kaoya"); } } } class Consumer implements Runnable { Resource r; Consumer(Resource r) { this.r = r; } public void run() { while(true) { r.out(); } } } public class ResourceDemo { public static void main(String[] args) { Resource r = new Resource(); Producer producer = new Producer(r); Consumer consumer = new Consumer(r); Thread t1 = new Thread(producer); Thread t2 = new Thread(consumer); Thread t3 = new Thread(producer); Thread t4 = new Thread(consumer); t1.start(); t2.start(); t3.start(); t4.start(); } }
运行结果,如丝般顺滑: