背景:生产者消费者的问题真的是绕不开,面试时候很可能让手写此代码,需要深入总结下。
实质上,很多后台服务程序并发控制的基本原理都可以归纳为生产者/消费者模式,而这是恰恰是在本科操作系统课堂上老师反复讲解,而我们却视而不见不以为然的。在博文《一种面向作业流(工作流)的轻量级可复用的异步流水开发框架的设计与实现》中将介绍一种生产者/消费者模式的具体应用。
生产者消费者问题是研究多线程程序时绕不开的经典问题之一,它描述是有一块缓冲区作为仓库,生产者可以将产品放入仓库,消费者则可以从仓库中取走产品。
解决生产者/消费者问题的方法可分为两类:
(1)采用某种机制保护生产者和消费者之间的同步;
(2)在生产者和消费者之间建立一个管道。
第一种方式有较高的效率,并且易于实现,代码的可控制性较好,属于常用的模式。第二种管道缓冲区不易控制,被传输数据对象不易于封装等,实用性不强。
因此本文只介绍同步机制实现的生产者/消费者问题。
同步问题核心在于:如何保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。Java语言在多线程编程上实现了完全对象化,提供了对同步机制的良好支持。
在Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。
(1)wait() / notify()方法
(2)await() / signal()方法
(3)BlockingQueue阻塞队列方法
(4)PipedInputStream / PipedOutputStream
本文只介绍最常用的前三种,第四种暂不做讨论,有兴趣的读者可以自己去网上找答案。
wait()和notify()方法的实现
wait() / nofity()方法是基类Object的两个方法,也就意味着所有Java类都会拥有这两个方法,这样,我们就可以为任何对象实现同步机制。
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
它们都属于 Object 的一部分,而不属于 Thread。
只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。
使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
1 /** 2 * Project Name:basic 3 * File Name:ProducerAndConsumerWaitNotifyAll.java 4 * Package Name:com.forwork.com.basic.thread0411 5 * Date:2019年4月11日上午6:45:33 6 * Copyright (c) 2019, 深圳金融电子结算中心 All Rights Reserved. 7 * 8 */ 9 10 package com.forwork.com.basic.thread0411; 11 12 /** 13 * ClassName:ProducerAndConsumerWaitNotifyAll <br/> 14 * Function: TODO <br/> 15 * Date: 2019年4月11日 上午6:45:33 <br/> 16 * @author Administrator 17 * @version 1.0 18 * @since JDK 1.7 19 * @see 20 */ 21 public class ProducerAndConsumerWaitNotifyAll { 22 23 private static int count = 0; 24 private static int FULL = 3; //等待条件 25 private static int EMPTY = 0; 26 private static String LOCK = "lock"; 27 28 private static class Producer implements Runnable { 29 public void run() { 30 for (int i = 0; i < 3; i++) { 31 try { 32 Thread.sleep(1000); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 synchronized (LOCK) { 37 if (count == FULL) { 38 System.out.println(Thread.currentThread().getName() + "producelock:" + count); 39 try { 40 LOCK.wait(); 41 } catch (InterruptedException e) { 42 e.printStackTrace(); 43 } 44 } 45 count++; 46 System.out.println(Thread.currentThread().getName() + "produce:" + count); 47 LOCK.notifyAll(); 48 49 } 50 } 51 } 52 } 53 54 private static class Consumer implements Runnable { 55 public void run() { 56 for (int i = 0; i < 3; i++) { 57 try { 58 Thread.sleep(1000); 59 } catch (InterruptedException e) { 60 e.printStackTrace(); 61 } 62 63 synchronized (LOCK) { 64 if (count == EMPTY) { 65 try { 66 System.out.println(Thread.currentThread().getName() + "consumerlock:" + count); 67 LOCK.wait(); 68 } catch (Exception e) { 69 e.printStackTrace(); 70 } 71 }// (count == EMPTY) 72 count--; 73 System.out.println(Thread.currentThread().getName() + "consumer:" + count); 74 LOCK.notifyAll(); 75 } 76 } 77 } 78 } 79 80 public static void main(String[] args) { 81 for (int i = 0; i < 5; i++) { 82 Producer producer = new Producer(); 83 new Thread(producer).start(); 84 } 85 86 for (int i = 0; i < 5; i++) { 87 Consumer consumer = new Consumer(); 88 new Thread(consumer).start(); 89 } 90 } 91 92 }
结果:
1 Thread-1produce:1 2 Thread-6consumer:0 3 Thread-5consumerlock:0 4 Thread-8consumerlock:0 5 Thread-9consumerlock:0 6 Thread-7consumerlock:0 7 Thread-4produce:1 8 Thread-0produce:2 9 Thread-3produce:3 10 Thread-2producelock:3 11 Thread-7consumer:2 12 Thread-9consumer:1 13 Thread-8consumer:0 14 Thread-5consumer:-1 15 Thread-2produce:0 16 Thread-1produce:1 17 Thread-6consumer:0 18 Thread-0produce:1 19 Thread-3produce:2 20 Thread-4produce:3 21 Thread-9consumer:2 22 Thread-7consumer:1 23 Thread-2produce:2 24 Thread-8consumer:1 25 Thread-5consumer:0 26 Thread-1produce:1 27 Thread-6consumer:0 28 Thread-0produce:1 29 Thread-4produce:2 30 Thread-3produce:3 31 Thread-2producelock:3 32 Thread-9consumer:2 33 Thread-8consumer:1 34 Thread-7consumer:0 35 Thread-5consumerlock:0 36 Thread-2produce:1 37 Thread-5consumer:0
生产者在缓冲区full后wait,等待消费者调用notifyAll()唤醒后继续生产;
消费者在缓冲区empty后wait,等待生产者调用notifyAll()唤醒后继续消费。
wait() 和 sleep() 的区别
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会。
可重入锁ReentrantLock的实现
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,通过对lock的lock()方法和unlock()方法实现了对锁的显示控制,而synchronize()则是对锁的隐性控制。
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响,简单来说,该锁维护这一个与获取锁相关的计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,函数调用结束计数器就减1,然后锁需要被释放两次才能获得真正释放。已经获取锁的线程进入其他需要相同锁的同步代码块不会被阻塞。
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
使用 Lock 来获取一个 Condition 对象。
1 package com.forwork.com.basic.thread0411; 2 3 import java.util.concurrent.locks.Condition; 4 import java.util.concurrent.locks.Lock; 5 import java.util.concurrent.locks.ReentrantLock; 6 7 /** 8 * ClassName:ProduceAndConsumerReenTrantLock <br/> 9 * Function: ReenTrantLock实现 10 * Date: 2019年4月11日 上午7:55:20 <br/> 11 * @author Administrator 12 * @version 1.0 13 * @since JDK 1.7 14 * @see 15 */ 16 public class ProduceAndConsumerReenTrantLock { 17 18 private static int count = 0; 19 private static int FULL = 3; //等待条件 20 private static int EMPTY = 0; 21 private static Lock clock = new ReentrantLock(); 22 private static Condition empty = clock.newCondition(); 23 private static Condition full = clock.newCondition(); 24 25 private static class Producer implements Runnable { 26 public void run() { 27 for (int i = 0; i < 3; i++) { 28 try { 29 Thread.sleep(1000); 30 } catch (InterruptedException e) { 31 e.printStackTrace(); 32 } 33 34 clock.lock(); 35 try { 36 if (count == FULL) { 37 System.out.println(Thread.currentThread().getName() + " producelock:" + count); 38 try { 39 full.await(); 40 } catch (InterruptedException e) { 41 e.printStackTrace(); 42 } 43 } 44 45 count++; 46 System.out.println(Thread.currentThread().getName() + " produce:" + count); 47 empty.signalAll(); //唤醒消费者 48 } finally { 49 clock.unlock(); 50 } 51 } 52 } 53 } 54 55 private static class Consumer implements Runnable { 56 public void run() { 57 for (int i = 0; i < 3; i++) { 58 try { 59 Thread.sleep(1000); 60 } catch (InterruptedException e) { 61 e.printStackTrace(); 62 } 63 clock.lock(); 64 try { 65 if (count == EMPTY) { 66 try { 67 System.out.println(Thread.currentThread().getName() + " consumerlock:" + count); 68 empty.await(); 69 } catch (Exception e) { 70 e.printStackTrace(); 71 } 72 }// (count == EMPTY) 73 count--; 74 System.out.println(Thread.currentThread().getName() + " consumer:" + count); 75 full.signalAll(); //唤醒生产者 76 } finally { 77 clock.unlock(); 78 } 79 } 80 } 81 } 82 83 public static void main(String[] args) { 84 for (int i = 0; i < 5; i++) { 85 Producer producer = new Producer(); 86 new Thread(producer).start(); 87 } 88 89 for (int i = 0; i < 5; i++) { 90 Consumer consumer = new Consumer(); 91 new Thread(consumer).start(); 92 } 93 } 94 95 }
结果:
Thread-1 produce:1 Thread-4 produce:2 Thread-0 produce:3 Thread-2 producelock:3 Thread-6 consumer:2 Thread-5 consumer:1 Thread-7 consumer:0 Thread-3 produce:1 Thread-9 consumer:0 Thread-8 consumerlock:0 Thread-2 produce:1 Thread-8 consumer:0 Thread-0 produce:1 Thread-1 produce:2 Thread-4 produce:3 Thread-5 consumer:2 Thread-6 consumer:1 Thread-9 consumer:0 Thread-7 consumerlock:0 Thread-3 produce:1 Thread-7 consumer:0 Thread-2 produce:1 Thread-8 consumer:0 Thread-4 produce:1 Thread-0 produce:2 Thread-1 produce:3 Thread-6 consumer:2 Thread-5 consumer:1 Thread-3 produce:2 Thread-7 consumer:1 Thread-9 consumer:0 Thread-2 produce:1 Thread-8 consumer:0
通过clock来newCondition()。
在try finally块中释放lock锁。
Condition 类上通过await()和signal() signalAll()实现线程的协同
三、BlockingQueue阻塞队列方法
BlockingQueue是JDK5.0的新增内容,它是一个已经在内部实现了同步的队列,实现方式采用的是我们第2种await() / signal()方法。它可以在生成对象时指定容量大小。它用于阻塞操作的是put()和take()方法。
put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞。
public class ProducerConsumer { private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5); private static class Producer extends Thread { @Override public void run() { try { queue.put("product"); } catch (InterruptedException e) { e.printStackTrace(); } System.out.print("produce.."); } } private static class Consumer extends Thread { @Override public void run() { try { String product = queue.take(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.print("consume.."); } } }
public static void main(String[] args) { for (int i = 0; i < 2; i++) { Producer producer = new Producer(); producer.start(); } for (int i = 0; i < 5; i++) { Consumer consumer = new Consumer(); consumer.start(); } for (int i = 0; i < 3; i++) { Producer producer = new Producer(); producer.start(); } }
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:
- 当队列满了的时候进行入队列操作
- 当队列空了的时候进行出队列操作
因此,当一个线程对已经满了的阻塞队列进行入队操作时会阻塞,除非有另外一个线程进行了出队操作,当一个线程对一个空的阻塞队列进行出队操作时也会阻塞,除非有另外一个线程进行了入队操作。
从上可知,阻塞队列是线程安全的。
下面是BlockingQueue接口的一些方法:
其实阻塞队列实现阻塞同步的方式很简单,使用的就是是lock锁的多条件(condition)阻塞控制。使用BlockingQueue封装了根据条件阻塞线程的过程,而我们就不用关心繁琐的await/signal操作了。
package com.forwork.com.basic.thread0411; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; /** * ClassName:ProduceAndConsumerBlockQueue <br/> * Function: TODO <br/> * Date: 2019年4月12日 上午6:50:14 <br/> * @author Administrator * @version 1.0 * @since JDK 1.7 * @see */ public class ProduceAndConsumerBlockQueue { private static BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(3); private static class Producer implements Runnable { public void run() { for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); queue.put(i); //入队 System.out.println(Thread.currentThread().getName() + " produce:" + queue.size()); } catch (Exception e) { e.printStackTrace(); } } } } private static class Consumer implements Runnable { public void run() { for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); queue.take(); //出队 System.out.println(Thread.currentThread().getName() + " consumer:" + queue.size()); } catch (Exception e) { e.printStackTrace(); } } } } public static void main(String[] args) { for (int i = 0; i < 3; i++) { Producer producer = new Producer(); new Thread(producer).start(); } for (int i = 0; i < 3; i++) { Consumer consumer = new Consumer(); new Thread(consumer).start(); } } }
1 Thread-0 produce:3 2 Thread-2 produce:0 3 Thread-1 produce:0 4 Thread-4 consumer:0 5 Thread-5 consumer:1 6 Thread-3 consumer:2 7 Thread-0 produce:1 8 Thread-3 consumer:0 9 Thread-5 consumer:0 10 Thread-4 consumer:0 11 Thread-1 produce:1 12 Thread-2 produce:2 13 Thread-0 produce:1 14 Thread-3 consumer:0 15 Thread-1 produce:1 16 Thread-2 produce:2 17 Thread-4 consumer:0 18 Thread-5 consumer:1
put和take采用阻塞的方式插入和取出元素。
当队列为空或者满的时候,线程会挂起,直到有元素放入或者取出时候才会继续执行。
Java并发编程-阻塞队列(BlockingQueue)的实现原理
信号量Semaphore的实现
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源,在操作系统中是一个非常重要的问题,可以用来解决哲学家就餐问题。Java中的Semaphore维护了一个许可集,一开始先设定这个许可集的数量,可以使用acquire()方法获得一个许可,当许可不足时会被阻塞,release()添加一个许可。在下列代码中,还加入了另外一个mutex信号量,维护生产者消费者之间的同步关系,保证生产者和消费者之间的交替进行
/** * Project Name:basic * File Name:ProduceAndConsumerSemaphore.java * Package Name:com.forwork.com.basic.thread0411 * Date:2019年4月12日上午8:05:07 * Copyright (c) 2019, 深圳金融电子结算中心 All Rights Reserved. * */ package com.forwork.com.basic.thread0411; import java.util.concurrent.Semaphore; /** * ClassName:ProduceAndConsumerSemaphore <br/> * Function: TODO <br/> * Date: 2019年4月12日 上午8:05:07 <br/> * @author Administrator * @version 1.0 * @since JDK 1.7 * @see */ public class ProduceAndConsumerSemaphore { private static Semaphore sp = new Semaphore(3); private static class Producer implements Runnable { public void run() { for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); sp.acquire(); //入队 System.out.println(Thread.currentThread().getName() + " produce:" + sp.availablePermits()); } catch (Exception e) { e.printStackTrace(); } } } } private static class Consumer implements Runnable { public void run() { for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); sp.release(); //出队 System.out.println(Thread.currentThread().getName() + " consumer:" + sp.availablePermits()); } catch (Exception e) { e.printStackTrace(); } } } } public static void main(String[] args) { for (int i = 0; i < 3; i++) { Producer producer = new Producer(); new Thread(producer).start(); } for (int i = 0; i < 3; i++) { Consumer consumer = new Consumer(); new Thread(consumer).start(); } } }
结果:
1 Thread-0 produce:0 2 Thread-2 produce:0 3 Thread-1 produce:0 4 Thread-3 consumer:1 5 Thread-4 consumer:3 6 Thread-5 consumer:3 7 Thread-2 produce:0 8 Thread-1 produce:0 9 Thread-0 produce:0 10 Thread-3 consumer:1 11 Thread-4 consumer:3 12 Thread-5 consumer:3 13 Thread-0 produce:1 14 Thread-2 produce:1 15 Thread-1 produce:0 16 Thread-3 consumer:1 17 Thread-4 consumer:3 18 Thread-5 consumer:3
/** * Project Name:basic * File Name:SemaphoreTest.java * Package Name:com.forwork.com.basic.thread0411 * Date:2019年4月12日上午8:07:48 * Copyright (c) 2019, 深圳金融电子结算中心 All Rights Reserved. * */ package com.forwork.com.basic.thread0411; import java.util.concurrent.Semaphore; /** * ClassName:SemaphoreTest <br/> * Function: Semaphore相当于一个队列,队列中可用的信号量为初始化分配的数量n。 * 每次release就多分配一个,acquire就消耗一个 <br/> * Date: 2019年4月12日 上午8:07:48 <br/> * @author Administrator * @version 1.0 * @since JDK 1.7 * @see */ public class SemaphoreTest { private static Semaphore sp = new Semaphore(0); public static void main(String[] args) { try { for (int i = 0; i < 3; i++) { System.out.println(sp.availablePermits() + ":one"); sp.release(); sp.release(); System.out.println(sp.availablePermits() + ":two"); sp.acquire(); System.out.println(sp.availablePermits() + ":three"); sp.acquire(); System.out.println(sp.availablePermits() + ":four"); } } catch (Exception e) { e.printStackTrace(); } } }
结果:
1 0:one 2 2:two 3 1:three 4 0:four 5 0:one 6 2:two 7 1:three 8 0:four 9 0:one 10 2:two 11 1:three 12 0:four
如何取得可用数量集的个数:sp.availablePermits()
每次release可用数量集会增加?是的,相当于BlockingQueue中的put操作
管道输入输出流PipedInputStream和PipedOutputStream实现
ps:了解
Java里的管道输入流 PipedInputStream与管道输出流 PipedOutputStream
感觉不是很好用~
在java的io包下,PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流。
它们的作用是让多线程可以通过管道进行线程间的通讯。在使用管道通信时,必须将PipedOutputStream和PipedInputStream配套使用。
使用方法:先创建一个管道输入流和管道输出流,然后将输入流和输出流进行连接,用生产者线程往管道输出流中写入数据,消费者在管道输入流中读取数据,这样就可以实现了不同线程间的相互通讯,但是这种方式在生产者和生产者、消费者和消费者之间不能保证同步,也就是说在一个生产者和一个消费者的情况下是可以生产者和消费者之间交替运行的,多个生成者和多个消费者者之间则不行
/** * 使用管道实现生产者消费者模型 * @author ZGJ * @date 2017年6月30日 */ public class Test5 { final PipedInputStream pis = new PipedInputStream(); final PipedOutputStream pos = new PipedOutputStream(); { try { pis.connect(pos); } catch (IOException e) { e.printStackTrace(); } } class Producer implements Runnable { @Override public void run() { try { while(true) { Thread.sleep(1000); int num = (int) (Math.random() * 255); System.out.println(Thread.currentThread().getName() + "生产者生产了一个数字,该数字为: " + num); pos.write(num); pos.flush(); } } catch (Exception e) { e.printStackTrace(); } finally { try { pos.close(); pis.close(); } catch (IOException e) { e.printStackTrace(); } } } } class Consumer implements Runnable { @Override public void run() { try { while(true) { Thread.sleep(1000); int num = pis.read(); System.out.println("消费者消费了一个数字,该数字为:" + num); } } catch (Exception e) { e.printStackTrace(); } finally { try { pos.close(); pis.close(); } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args) { Test5 test5 = new Test5(); new Thread(test5.new Producer()).start(); new Thread(test5.new Consumer()).start(); } }