生产者消费者模型的正确姿势
简介:
生产者、消费者模型是多线程编程的常见问题,最简单的一个生产者、一个消费者线程模型大多数人都能够写出来,但是一旦条件发生变化,我们就很容易掉进多线程的bug中。这篇文章主要讲解了生产者和消费者的数量,商品缓存位置数量,商品数量等多个条件的不同组合下,写出正确的生产者消费者模型的方法。
欢迎探讨,如有错误敬请指正
如需转载,请注明出处 http://www.cnblogs.com/nullzx/
定义商品类
package demo; /*定义商品*/ public class Goods { public final String name; public final int price; public final int id; public Goods(String name, int price, int id){ this.name = name; /*类型*/ this.price = price; /*价格*/ this.id = id; /*商品序列号*/ } @Override public String toString(){ return "name: " + name + ", price:"+ price + ", id: " + id; } }
基本要求:
1)生产者不能重复生产一个商品,也就是说不能有两个id相同的商品
2)生产者不能覆盖一个商品(当前商品还未被消费,就被下一个新商品覆盖)。也就是说消费商品时,商品的id属性可以不连续,但不能出现缺号的情况
3)消费者不能重复消费一个商品
1. 生产者线程无限生产,消费者线程无限消费 的模式
1.1使用线程对象,一个生产者线程,一个消费者线程,一个商品存储位置
package demo; import java.util.Random; /*使用线程对象,一个缓存位置,一个生产者,一个消费者,无限生产商品消费商品*/ public class ProducterComsumerDemo1 { /*定义一个商品缓存位置*/ private volatile Goods goods; /*定义一个对象作为锁,不使用goods作为锁是因为生产者每次会产生一个新的对象*/ private Object obj = new Object(); /*isFull == true 生产者线程休息,消费者线程消费 *isFull == false 消费者线程休息,生产者线程生产*/ private volatile boolean isFull = false; /*商品的id编号,生产者制造的每个商品的id都不一样,每生产一个id自增1*/ private int id = 1; /*随机产生一个sleep时间*/ private Random rnd = new Random(); /*=================定义消费者线程==================*/ public class ComsumeThread implements Runnable{ @Override public void run(){ try{ while(true){ /*获取obj对象的锁, id 和 isFull 的操作都在同步代码块中*/ synchronized(obj){ if(!isFull){ /*wait方法使当前线程阻塞,并释放锁*/ obj.wait(); } /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); /*模拟消费商品*/ System.out.println(goods); /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); isFull = false; /*唤醒阻塞obj上的生产者线程*/ obj.notify(); } /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); } }catch (InterruptedException e){ /*什么都不做*/ } } } /*=================定义生产者线程==================*/ public class ProductThread implements Runnable{ @Override public void run(){ try { while(true){ synchronized(obj){ if(isFull){ obj.wait(); } Thread.sleep(rnd.nextInt(500)); /*如果id为偶数,生产价格为2的产品A *如果id为奇数,生产价格为1的产品B*/ if(id % 2 == 0){ goods = new Goods("A", 2, id); }else{ goods = new Goods("B", 1, id); } Thread.sleep(rnd.nextInt(250)); id++; isFull = true; /*唤醒阻塞的消费者线程*/ obj.notify(); } } } catch (InterruptedException e) { /*什么都不做*/ } } } public static void main(String[] args) throws InterruptedException{ ProducterComsumerDemo1 pcd = new ProducterComsumerDemo1(); Runnable c = pcd.new ComsumeThread(); Runnable p = pcd.new ProductThread(); new Thread(p).start(); new Thread(c).start(); } }
运行结果
name: B, price:1, id: 1 name: A, price:2, id: 2 name: B, price:1, id: 3 name: A, price:2, id: 4 name: B, price:1, id: 5 name: A, price:2, id: 6 name: B, price:1, id: 7 name: A, price:2, id: 8 name: B, price:1, id: 9 name: A, price:2, id: 10 name: B, price:1, id: 11 name: A, price:2, id: 12 name: B, price:1, id: 13 ……
从结果看出,商品类型交替生产,每个商品的id都不相同,且不会漏过任何一个id,生产者没有重复生产,消费者没有重复消费,结果完全正确。
1.2. 使用线程对象,多个生产者线程,多个消费者线程,1个缓存位置
1.2.1一个经典的bug
对于多生产者,多消费者这个问题,看起来我们似乎不用修改代码,只需在main方法中多添加几个线程就好。假设我们需要三个消费者,一个生产者,那么我们只需要在main方法中再添加两个消费者线程。
public static void main(String[] args) throws InterruptedException{ ProducterComsumerDemo1 pcd = new ProducterComsumerDemo1(); Runnable c = pcd.new ComsumeThread(); Runnable p = pcd.new ProductThread(); new Thread(c).start(); new Thread(p).start(); new Thread(c).start(); new Thread(c).start(); }
运行结果
name: B, price:1, id: 1 name: A, price:2, id: 2 name: A, price:2, id: 2 name: B, price:1, id: 3 name: B, price:1, id: 3 name: A, price:2, id: 4 name: A, price:2, id: 4 name: B, price:1, id: 5 name: B, price:1, id: 5 name: A, price:2, id: 6 ……
从结果中,我们发现消费者重复消费了商品,所以这样做显然是错误的。这里我们定义多个消费者,一个生产者,所以遇到了重复消费的问题,如果定义成一个消费者,多个生产者就会遇到id覆盖的问题。如果我们定义多个消费者,多个生产者,那么即会遇到重复消费,也会遇到id覆盖的问题。注意,上面的代码使用的notifyAll唤醒方法,如果使用notify方法唤醒bug仍然可能发生。
现在我们来分析一下原因。当生产者生产好了商品,会唤醒因没有商品而阻塞消费者线程,假设唤醒的消费者线程超过两个,这两个线程会竞争获取锁,获取到锁的线程就会从obj.wait()方法中返回,然后消费商品,并把isFull置为false,然后释放锁。当被唤醒的另一个线程竞争获取到锁了以后也会从obj.wait()方法中返回。会再次消费同一个商品。显然,每一个被唤醒的线程应该再次检查isFull这个条件。所以无论是消费者,还是生产者,isFull的判断必须改成while循环,这样才能得到正确的结果而不受生产者的线程数和消费者的线程数的影响。
而对于只有一个生产者线程,一个消费者线程,用if判断是没有问题的,但是仍然强烈建议改成while语句进行判断。
1.2.2正确的姿势
package demo; import java.util.Random; /*使用线程对象,一个缓存位置,一个生产者,一个消费者,无限生产商品消费商品*/ public class ProducterComsumerDemo1 { /*定义一个商品缓存位置*/ private volatile Goods goods; /*定义一个对象作为锁,不使用goods作为锁是因为生产者每次会产生一个新的对象*/ private Object obj = new Object(); /*isFull == true 生产者线程休息,消费者线程消费 *isFull == false 消费者线程消费,生产者线程生产*/ private volatile boolean isFull = false; /*商品的id编号,生产者制造的每个商品的id都不一样,每生产一个id自增1*/ private int id = 1; /*随机产生一个sleep时间*/ private Random rnd = new Random(); /*=================定义消费者线程==================*/ public class ComsumeThread implements Runnable{ @Override public void run(){ try{ while(true){ /*获取obj对象的锁, id 和 isFull 的操作都在同步代码块中*/ synchronized(obj){ while(!isFull){ /*wait方法使当前线程阻塞,并释放锁*/ obj.wait(); } /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); /*模拟消费商品*/ System.out.println(goods); /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); isFull = false; /*唤醒阻塞obj上的生产者线程*/ obj.notifyAll(); } /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); } }catch (InterruptedException e){ /*我就是任性,这里什么都不做*/ } } } /*=================定义生产者线程==================*/ public class ProductThread implements Runnable{ @Override public void run(){ try { while(true){ synchronized(obj){ while(isFull){ obj.wait(); } Thread.sleep(rnd.nextInt(500)); /*如果id为偶数,生产价格为2的产品A 如果id为奇数,生产价格为1的产品B*/ if(id % 2 == 0){ goods = new Goods("A", 2, id); }else{ goods = new Goods("B", 1, id); } Thread.sleep(rnd.nextInt(250)); id++; isFull = true; /*唤醒阻塞的消费者线程*/ obj.notifyAll(); } } } catch (InterruptedException e) { /*我就是任性,这里什么都不做*/ } } } public static void main(String[] args) throws InterruptedException{ ProducterComsumerDemo1 pcd = new ProducterComsumerDemo1(); Runnable c = pcd.new ComsumeThread(); Runnable p = pcd.new ProductThread(); new Thread(p).start(); new Thread(p).start(); new Thread(p).start(); new Thread(c).start(); new Thread(c).start(); new Thread(c).start(); } }
1.3 使用线程对象,多个缓存位置(有界),多生产者,多消费者
1)当缓存位置满时,我们应该阻塞生产者线程
2)当缓存位置空时,我们应该阻塞消费者线程
下面的代码我没有用java对象内置的锁,而是用了ReentrantLock对象。是因为普通对象的锁只有一个阻塞队列,如果使用notify方式,无法保证唤醒的就是特定类型的线程(消费者线程或生产者线程),而notifyAll方法会唤醒所有的线程,当剩余的缓存商品的数量小于生产者线程数量或已缓存商品的数量小于消费者线程时效率就比较低。所以这里我们通过ReentrantLock对象构造两个阻塞队列提高效率。
1.3.1 普通方式
package demo; import java.util.LinkedList; import java.util.Random; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /*使用线程对象,多个缓存位置(有界),多生产者,多消费者,无限循环模式*/ public class ProducterComsumerDemo2 { /*最大缓存商品数*/ private final int MAX_SLOT = 2; /*定义缓存商品的容器*/ private LinkedList<Goods> queue = new LinkedList<Goods>(); /*定义线程锁和锁对应的阻塞队列*/ private Lock lock = new ReentrantLock(); private Condition full = lock.newCondition(); private Condition empty = lock.newCondition(); /*商品的id编号,生产者制造的每个商品的id都不一样,每生产一个id自增1*/ private int id = 1; /*随机产生一个sleep时间*/ private Random rnd = new Random(); /*=================定义消费者线程==================*/ public class ComsumeThread implements Runnable{ @Override public void run(){ while(true){ /*加锁,queue的出列操作都在同步代码块中*/ lock.lock(); try { while(queue.isEmpty()){ System.out.println("queue is empty"); empty.await(); } /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(200)); /*模拟消费商品*/ Goods goods = queue.remove(); System.out.println(goods); /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(200)); /*唤醒阻塞的生产者线程*/ full.signal(); } catch (InterruptedException e) { /*什么都不做*/ }finally{ lock.unlock(); } /*释放锁后随机延时一段时间*/ try { Thread.sleep(rnd.nextInt(200)); } catch (InterruptedException e) { /*什么都不做*/ } } } } /*=================定义生产者线程==================*/ public class ProductThread implements Runnable{ @Override public void run(){ while(true){ /*加锁,queue的入列操作,id操作都在同步代码块中*/ lock.lock(); try{ while(queue.size() == MAX_SLOT){ System.out.println("queue is full"); full.await(); } Thread.sleep(rnd.nextInt(200)); Goods goods = null; /*根据序号产生不同的商品*/ switch(id%3){ case 0 : goods = new Goods("A", 1, id); break; case 1 : goods = new Goods("B", 2, id); break; case 2 : goods = new Goods("C", 3, id); break; } Thread.sleep(rnd.nextInt(200)); queue.add(goods); id++; /*唤醒阻塞的消费者线程*/ empty.signal(); }catch(InterruptedException e){ /*什么都不做*/ }finally{ lock.unlock(); } /*释放锁后随机延时一段时间*/ try { Thread.sleep(rnd.nextInt(100)); } catch (InterruptedException e) { /*什么都不做*/ } } } } /*=================main==================*/ public static void main(String[] args) throws InterruptedException{ ProducterComsumerDemo2 pcd = new ProducterComsumerDemo2(); Runnable c = pcd.new ComsumeThread(); Runnable p = pcd.new ProductThread(); /*两个生产者线程,两个消费者线程*/ new Thread(p).start(); new Thread(p).start(); new Thread(c).start(); new Thread(c).start(); } }
运行结果
queue is empty queue is empty name: B, price:2, id: 1 name: C, price:3, id: 2 name: A, price:1, id: 3 queue is full name: B, price:2, id: 4 name: C, price:3, id: 5 queue is full name: A, price:1, id: 6 name: B, price:2, id: 7 name: C, price:3, id: 8 name: A, price:1, id: 9 name: B, price:2, id: 10 name: C, price:3, id: 11 name: A, price:1, id: 12 name: B, price:2, id: 13 name: C, price:3, id: 14 ……
1.3.2 更优雅的实现方式
下面使用线程池(ThreadPool)和阻塞队列(LinkedBlockingQueue)原子类(AtomicInteger)以更加优雅的方式实现上述功能。LinkedBlockingQueue阻塞队列仅在take和put方法上锁,所以id必须定义为原子类。
package demo; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; /*使用线程对象,多个缓存位置(有界),多生产者,多消费者,无限循环模式*/ public class ProducterComsumerDemo4 { /*最大缓存商品数*/ private final int MAX_SLOT = 3; /*定义缓存商品的容器*/ private LinkedBlockingQueue<Goods> queue = new LinkedBlockingQueue<Goods>(MAX_SLOT); /*商品的id编号,生产者制造的每个商品的id都不一样,每生产一个id自增1*/ private AtomicInteger id = new AtomicInteger(1); /*随机产生一个sleep时间*/ private Random rnd = new Random(); /*=================定义消费者线程==================*/ public class ComsumeThread implements Runnable{ @Override public void run(){ while(true){ try { /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(200)); /*模拟消费商品*/ Goods goods = queue.take(); System.out.println(goods); /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(200)); } catch (InterruptedException e) { /*什么都不做*/ } } } } /*=================定义生产者线程==================*/ public class ProductThread implements Runnable{ @Override public void run(){ while(true){ try{ int x = id.getAndIncrement(); Goods goods = null; Thread.sleep(rnd.nextInt(200)); /*根据序号产生不同的商品*/ switch(x%3){ case 0 : goods = new Goods("A", 1, x); break; case 1 : goods = new Goods("B", 2, x); break; case 2 : goods = new Goods("C", 3, x); break; } Thread.sleep(rnd.nextInt(200)); queue.put(goods); Thread.sleep(rnd.nextInt(100)); }catch(InterruptedException e){ /*什么都不做*/ } } } } /*=================main==================*/ public static void main(String[] args) throws InterruptedException{ ProducterComsumerDemo4 pcd = new ProducterComsumerDemo4(); Runnable c = pcd.new ComsumeThread(); Runnable p = pcd.new ProductThread(); /*定义线程池*/ ExecutorService es = Executors.newCachedThreadPool(); /*三个生产者线程,两个消费者线程*/ es.execute(p); es.execute(p); es.execute(p); es.execute(c); es.execute(c); es.shutdown(); } }
2. 有限商品个数
这个问题显然比上面的问题要复杂不少,原因在于要保证缓存区的商品要全部消费掉,没有重复消费商品,没有覆盖商品,同时还要保证所有线程能够正常结束,防止存在一直阻塞的线程。
2.1 使用线程对象,多个缓存位置(有界),多生产者,多消费者
思路 定义一下三个变量
/*需要生产的总商品数*/ private final int TOTAL_NUM = 30; /*已产生的数量*/ private volatile int productNum = 0; /*已消耗的商品数*/ private volatile int comsumedNum = 0;
每生产一个商品 productNum 自增1,直到TOTAL_NUM为止,如果不满足条件 productNum < TOTAL_NUM 则结束进程,自增操作必须在full.await()方法调用之前,防止生产者线程无法唤醒。
同理,每消费一个商品 comsumedNum 自增1,直到TOTAL_NUM为止,如果不满足条件 comsumedNum < TOTAL_NUM 则结束进程,自增操作必须在empty.await()方法调用之前,防止消费者线程无法唤醒。
comsumedNum和productNum相当于计划经济时代的粮票一样,有了它能够保证生产者线程在唤醒后一定需要生产一个商品,消费者线程在唤醒以后一定能够消费一个商品
package demo; import java.util.LinkedList; import java.util.Random; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /*使用线程对象,多个缓存位置(有界),多生产者,多消费者, 有限商品个数*/ public class ProducterComsumerDemo3 { /*需要生产的总商品数*/ private final int TOTAL_NUM = 30; /*已产生的数量*/ private volatile int productNum = 0; /*已消耗的商品数*/ private volatile int comsumedNum = 0; /*最大缓存商品数*/ private final int MAX_SLOT = 2; /*定义线程公用的锁和条件*/ private Lock lock = new ReentrantLock(); private Condition full = lock.newCondition(); private Condition empty = lock.newCondition(); /*定义缓存商品的容器*/ private LinkedList<Goods> queue = new LinkedList<Goods>(); /*商品的id编号,生产者制造的每个商品的id都不一样,每生产一个id自增1*/ private int id = 1; /*随机产生一个sleep时间*/ private Random rnd = new Random(); /*=================定义消费者线程==================*/ public class ComsumeThread implements Runnable{ @Override public void run(){ while(true){ /*加锁, id、comsumedNum 操作都在同步代码块中*/ lock.lock(); try { /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); if(comsumedNum < TOTAL_NUM){ comsumedNum++; }else{ /*这里会自动执行finally的语句,释放锁*/ break; } while(queue.isEmpty()){ System.out.println("queue is empty"); empty.await(); } /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); /*模拟消费商品*/ Goods goods = queue.remove(); System.out.println(goods); /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); /*唤醒阻塞的生产者线程*/ full.signal(); } catch (InterruptedException e) { }finally{ lock.unlock(); } /*释放锁后,随机延时一段时间*/ try { Thread.sleep(rnd.nextInt(250)); } catch (InterruptedException e) { } } System.out.println( "customer " + Thread.currentThread().getName() + " is over"); } } /*=================定义生产者线程==================*/ public class ProductThread implements Runnable{ @Override public void run(){ while(true){ lock.lock(); try{ /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(250)); if(productNum < TOTAL_NUM){ productNum++; }else{ /*这里会自动执行finally的语句,释放锁*/ break; } Thread.sleep(rnd.nextInt(250)); while(queue.size() == MAX_SLOT){ System.out.println("queue is full"); full.await(); } Thread.sleep(rnd.nextInt(250)); Goods goods = null; /*根据序号产生不同的商品*/ switch(id%3){ case 0 : goods = new Goods("A", 1, id); break; case 1 : goods = new Goods("B", 2, id); break; case 2 : goods = new Goods("C", 3, id); break; } queue.add(goods); id++; /*唤醒阻塞的消费者线程*/ empty.signal(); }catch(InterruptedException e){ }finally{ lock.unlock(); } /*释放锁后,随机延时一段时间*/ try { Thread.sleep(rnd.nextInt(250)); } catch (InterruptedException e) { /*什么都不做*/ } } System.out.println( "producter " + Thread.currentThread().getName() + " is over"); } } /*=================main==================*/ public static void main(String[] args) throws InterruptedException{ ProducterComsumerDemo3 pcd = new ProducterComsumerDemo3(); ComsumeThread c = pcd.new ComsumeThread(); ProductThread p = pcd.new ProductThread(); new Thread(p).start(); new Thread(p).start(); new Thread(p).start(); new Thread(c).start(); new Thread(c).start(); new Thread(c).start(); System.out.println("main Thread is over"); } }
2.2利用线程池,原子类,阻塞队列,以更优雅的方式实现
LinkedBlockingQueue阻塞队列仅在take和put方法上锁,所以productNum和comsumedNum必须定义为原子类。
package demo; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; /*使用线程池,多个缓存位置(有界),多生产者,多消费者, 有限商品个数*/ public class LinkedBlockingQueueDemo { /*需要生产的总商品数*/ private final int TOTAL_NUM = 20; /*已产生商品的数量*/ volatile AtomicInteger productNum = new AtomicInteger(0); /*已消耗的商品数*/ volatile AtomicInteger comsumedNum = new AtomicInteger(0); /*最大缓存商品数*/ private final int MAX_SLOT = 5; /*同步阻塞队列,队列容量为MAX_SLOT*/ private LinkedBlockingQueue<Goods> lbq = new LinkedBlockingQueue<Goods>(MAX_SLOT); /*随机数*/ private Random rnd = new Random(); /*pn表示产品的编号,产品编号从1开始*/ private volatile AtomicInteger pn = new AtomicInteger(1); /*=================定义消费者线程==================*/ public class CustomerThread implements Runnable{ @Override public void run(){ while(comsumedNum.getAndIncrement() < TOTAL_NUM){ try{ /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(500)); /*从队列中取出商品,队列空时发生阻塞*/ Goods goods = lbq.take(); /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(500)); /*模拟消耗商品*/ System.out.println(goods); /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(500)); }catch(InterruptedException e){ } } System.out.println( "customer " + Thread.currentThread().getName() + " is over"); } } /*=================定义生产者线程==================*/ public class ProducerThread implements Runnable{ @Override public void run(){ while(productNum.getAndIncrement() < TOTAL_NUM){ try { int x = pn.getAndIncrement(); Goods goods = null; /*根据序号产生不同的商品*/ switch(x%3){ case 0 : goods = new Goods("A", 1, x); break; case 1 : goods = new Goods("B", 2, x); break; case 2 : goods = new Goods("C", 3, x); break; } /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(500)); /*产生的新产品入列,队列满时发生阻塞*/ lbq.put(goods); /*随机延时一段时间*/ Thread.sleep(rnd.nextInt(500)); } catch (InterruptedException e1) { /*什么都不做*/ } } System.out.println( "producter " + Thread.currentThread().getName() + " is over "); } } /*=================main==================*/ public static void main(String[] args){ LinkedBlockingQueueDemo lbqd = new LinkedBlockingQueueDemo(); Runnable c = lbqd.new CustomerThread(); Runnable p = lbqd.new ProducerThread(); ExecutorService es = Executors.newCachedThreadPool(); es.execute(c); es.execute(c); es.execute(c); es.execute(p); es.execute(p); es.execute(p); es.shutdown(); System.out.println("main Thread is over"); } }