今天看了一片博文,讲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(); } } } }
改进说明:改进后的生产者和消费者分别加了生产者锁和消费者锁,分别用于锁定同一时间只能有一个消费者(生产者)进入生产临界区(如果同时又两个生产者进入临界区,那么很有可能其中一个生产者本想唤醒消费者却唤醒了生产者),总的来说就是一共三个锁:消费者锁、生产者锁、生产者和消费者共享的锁。
最终,写多线程的时候需要注意的是一个资源可能唤醒的是所有因该资源而等待的线程,因此消费者线程不一定唤醒的就是生产者线程也可能是消费者线程。