今天看了一片博文,讲Java多线程之线程的协作,当中作者用程序实例说明了生产者和消费者问题,但我及其它读者发现程序多跑几次还是会出现死锁,百度搜了下大都数的样例也都存在bug,经过细致研究发现当中的问题。并攻克了,感觉有意义贴出来分享下。
以下首先贴出的是有bug的代码,一个4个类。Plate.java:
package CreatorAndConsumer; import java.util.ArrayList; import java.util.List; /** * 盘子,表示共享的资源 * @author Martin * */ public class Plate { private List<Object> eggs = new ArrayList<Object>(); /** * 获取蛋 * @return */ public Object getEgg() { System.out.println("消费者取蛋"); Object egg = eggs.get(0); eggs.remove(0); return egg; } /** * 增加蛋 * @return */ public void addEgg(Object egg) { System.out.println("生产者生蛋"); eggs.add(egg); } /** * 获取蛋个数 * @return */ public int getEggNum() { return eggs.size(); } }消费者类:Consumer2.java
package CreatorAndConsumer; public class Consumer2 implements Runnable { /** * 线程资源 */ private Plate plate; public Consumer2(Plate plate) { this.plate = plate; } @Override public void run() { synchronized (plate) { // 假设此时蛋的个数大于0。则等等 while (plate.getEggNum() < 1) { try { // 这个细节须要注意,假设线程进入wait,那么其上的锁就会临时得到释放。 // 不然其它线程也不能进行加锁,然后唤醒本线程 plate.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 唤醒后。再次得到资源锁。且条件满足就能够放心地取蛋了 plate.getEgg(); plate.notify(); } } }
生产者类:Creator2.java
package CreatorAndConsumer; /** * 生产者 * * @author Martin * */ public class Creator2 implements Runnable { /** * 线程资源 */ private Plate plate; public Creator2(Plate plate) { this.plate = plate; } @Override public void run() { synchronized (plate) { // 假设此时蛋的个数大于0,则等等 while (plate.getEggNum() >= 5) { try { // 这个细节须要注意,假设线程进入wait。那么其上的锁就会临时得到释放。 // 不然其它线程也不能进行加锁。然后唤醒本线程 plate.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 唤醒后,再次得到资源锁,且条件满足就能够放心地生蛋啦 Object egg = new Object(); plate.addEgg(egg); plate.notify(); } } }
測试类:Tester.java
package CreatorAndConsumer; public class Tester { public static void main(String[] args) { //共享资源 Plate plate = new Plate(); //加入生产者和消费者 for(int i = 0 ; i < 100; i ++) { //有bug版 new Thread(new Creator2(plate)).start(); new Thread(new Consumer2(plate)).start(); //无bug版 //new Thread(new Creator(plate)).start(); //new Thread(new Consumer(plate)).start(); } } }
假设多执行几次或者将測试类中的循环次数改大,则会发现出现死锁的概率还是非常高的。以下分析发生这样的问题的解决办法:
在jdk中对于Object.wait有这种一段解释:当前线程必须拥有此对象监视器。该线程放弃对此监视器的全部权并等待,直到其它线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。
可见以下一种情况就可能出现:消费者1进入等待状态(此时资源锁已被放开),假设此时消费者2获取到了资源同步锁(没有人保证消费者1进入等待,下一个拿到锁的一定是生产者)。消费者2推断没有资源也进入等待状态。此时生产者1生产了,并notify了消费者1,消费者1顺利地消费了,并运行notify操作,但此时消费者2却也由于资源而处于等待状态。从而唤醒了消费者2(消费者1本欲唤醒其它生产者)。而此时并没有不论什么资源。导致了整个程序由于消费者2陷入无限的等待。形成了死锁。
经过以上分析。究其根本原因是:同一时候几个消费者或几个生产者处于等待状态,导致消费者可能唤醒的还是消费者,或者生产者唤醒的还是生产者。那么假设我们可以保证同一时候仅仅有一个消费者处于wait状态(生产者同理),那就就能保证消费者唤醒的一定是生产者,从而能使整个任务顺利进行下去。以下是改动后的代码:
改进后的Consumer.java
package CreatorAndConsumer; public class Consumer implements Runnable { /** * 线程资源 */ private Plate plate; /** * 生产者锁:用于锁定同一时间仅仅能有一个生产者进入生产临界区(假设同一时候又两个生产者进入临界区。那么非常有可能当中一个生产者本想唤醒消费者却唤醒了生产者) */ private static Object consumerLocker = new Object(); public Consumer(Plate plate) { this.plate = plate; } @Override public void run() { // 必须先获得生产者锁才干生产 synchronized (consumerLocker) { synchronized (plate) { // 假设此时蛋的个数大于0,则等等 while (plate.getEggNum() < 1) { try { // 这个细节须要注意,假设线程进入wait,那么其上的锁就会临时得到释放。 // 不然其它线程也不能进行加锁,然后唤醒本线程 plate.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 唤醒后,再次得到资源锁,且条件满足就能够放心地取蛋了 plate.getEgg(); plate.notify(); } } } }
改进后的Creator.java:
package CreatorAndConsumer; /** * 生产者 * * @author Martin * */ public class Creator implements Runnable { /** * 线程资源 */ private Plate plate; /** * 生产者锁:用于锁定同一时间仅仅能有一个生产者进入生产临界区(假设同一时候又两个生产者进入临界区。那么非常有可能当中一个生产者本想唤醒消费者却唤醒了生产者) */ private static Object creatorLocker = new Object(); public Creator(Plate plate) { this.plate = plate; } @Override public void run() { //必须先获得生产者锁才干生产 synchronized (creatorLocker) { synchronized (plate) { // 假设此时蛋的个数大于0,则等等 while (plate.getEggNum() >= 5) { try { // 这个细节须要注意。假设线程进入wait,那么其上的锁就会临时得到释放。 // 不然其它线程也不能进行加锁,然后唤醒本线程 plate.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 唤醒后,再次得到资源锁,且条件满足就能够放心地生蛋啦 Object egg = new Object(); plate.addEgg(egg); plate.notify(); } } } }
改进说明:改进后的生产者和消费者分别加了生产者锁和消费者锁。分别用于锁定同一时间仅仅能有一个消费者(生产者)进入生产临界区(假设同一时候又两个生产者进入临界区,那么非常有可能当中一个生产者本想唤醒消费者却唤醒了生产者),总的来说就是一共三个锁:消费者锁、生产者锁、生产者和消费者共享的锁。
终于,写多线程的时候须要注意的是一个资源可能唤醒的是全部因该资源而等待的线程,因此消费者线程不一定唤醒的就是生产者线程也可能是消费者线程。