生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
- 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
- 如果共享数据区为空的话,阻塞消费者继续消费数据;
在实现生产者消费者问题时,可以采用三种方式:
1.使用Object的wait/notify的消息通知机制;
2.使用Lock的Condition的await/signal的消息通知机制;
3.使用BlockingQueue实现。本文主要将这三种实现方式进行总结归纳。
1. wait/notify的消息通知机制
1.1 预备知识
Java 中,可以通过配合调用 Object 对象的 wait() 方法和 notify()方法或 notifyAll() 方法来实现线程间的通信。在线程中调用 wait() 方法,将阻塞当前线程,直至等到其他线程调用了调用 notify() 方法或 notifyAll() 方法进行通知之后,当前线程才能从wait()方法出返回,继续执行下面的操作。
-
wait
该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象监视器锁,即只能在同步方法或同步块中调用 wait()方法。调用wait()方法之后,当前线程会释放锁。如果调用wait()方法时,线程并未获取到锁的话,则会抛出IllegalMonitorStateException异常,这是以个RuntimeException。如果再次获取到锁的话,当前线程才能从wait()方法处成功返回。
-
notify
该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。
该方法任意从WAITTING状态的线程中挑选一个进行通知,使得调用wait()方法的线程从等待队列移入到同步队列中,等待有机会再一次获取到锁,从而使得调用wait()方法的线程能够从wait()方法处退出。调用notify后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁。 -
notifyAll
该方法与 notify ()方法的工作方式相同,重要的一点差异是:
notifyAll 使所有原来在该对象上 wait 的线程统统退出WAITTING状态,使得他们全部从等待队列中移入到同步队列中去,等待下一次能够有机会获取到对象监视器锁。
wait()和notify()方法的实现
这也是最简单最基础的实现,缓冲区满和为空时都调用wait()方法等待,当生产者生产了一个产品或者消费者消费了一个产品之后会唤醒所有线程。
-
/**
-
* 生产者和消费者,wait()和notify()的实现
-
* @author ZGJ
-
* @date 2017年6月22日
-
*/
-
public class Test1 {
-
private static Integer count = 0;
-
private static final Integer FULL = 10;
-
private static String LOCK = "lock";
-
public static void main(String[] args) {
-
Test1 test1 = new Test1();
-
new Thread(test1.new Producer()).start();
-
new Thread(test1.new Consumer()).start();
-
new Thread(test1.new Producer()).start();
-
new Thread(test1.new Consumer()).start();
-
new Thread(test1.new Producer()).start();
-
new Thread(test1.new Consumer()).start();
-
new Thread(test1.new Producer()).start();
-
new Thread(test1.new Consumer()).start();
-
}
-
class Producer implements Runnable {
-
@Override
-
public void run() {
-
for (int i = 0; i < 10; i++) {
-
try {
-
Thread.sleep(3000);
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
synchronized (LOCK) {
-
while (count == FULL) {
-
try {
-
LOCK.wait();
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}
-
count++;
-
System.out.println(Thread.currentThread().getName() + "生产者生产,目前总共有" + count);
-
LOCK.notifyAll();
-
}
-
}
-
}
-
}
-
class Consumer implements Runnable {
-
@Override
-
public void run() {
-
for (int i = 0; i < 10; i++) {
-
try {
-
Thread.sleep(3000);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
synchronized (LOCK) {
-
while (count == 0) {
-
try {
-
LOCK.wait();
-
} catch (Exception e) {
-
}
-
}
-
count--;
-
System.out.println(Thread.currentThread().getName() + "消费者消费,目前总共有" + count);
-
LOCK.notifyAll();
-
}
-
}
-
}
-
}
-
}
结果:
-
Thread-0生产者生产,目前总共有1
-
Thread-4生产者生产,目前总共有2
-
Thread-3消费者消费,目前总共有1
-
Thread-1消费者消费,目前总共有0
-
Thread-2生产者生产,目前总共有1
-
Thread-6生产者生产,目前总共有2
-
Thread-7消费者消费,目前总共有1
-
Thread-5消费者消费,目前总共有0
-
Thread-0生产者生产,目前总共有1
-
Thread-4生产者生产,目前总共有2
-
Thread-3消费者消费,目前总共有1
-
Thread-6生产者生产,目前总共有2
-
Thread-1消费者消费,目前总共有1
-
Thread-7消费者消费,目前总共有0
-
Thread-2生产者生产,目前总共有1
-
Thread-5消费者消费,目前总共有0
-
Thread-0生产者生产,目前总共有1
-
Thread-4生产者生产,目前总共有2
-
Thread-3消费者消费,目前总共有1
-
Thread-7消费者消费,目前总共有0
-
Thread-6生产者生产,目前总共有1
-
Thread-2生产者生产,目前总共有2
-
Thread-1消费者消费,目前总共有1
-
Thread-5消费者消费,目前总共有0
-
Thread-0生产者生产,目前总共有1
-
Thread-4生产者生产,目前总共有2
-
Thread-3消费者消费,目前总共有1
-
Thread-1消费者消费,目前总共有0
-
Thread-6生产者生产,目前总共有1
-
Thread-7消费者消费,目前总共有0
-
Thread-2生产者生产,目前总共有1
-
注意:
-
notifyAll()方法可使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的哪个线程最先执行,但也有可能是随机执行的,这要取决于JVM虚拟机的实现。即最终也只有一个线程能被运行,上述线程优先级都相同,每次运行的线程都不确定是哪个,后来给线程设置优先级后也跟预期不一样,还是要看JVM的具体实现吧。
await() / signal()方法
可重入锁ReentrantLock的实现
在JDK5中,用ReentrantLock和Condition可以实现等待/通知模型,具有更大的灵活性。通过在Lock对象上调用newCondition()方法,将条件变量和一个锁对象进行绑定,进而控制并发程序访问竞争资源的安全。
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,通过对lock的lock()方法和unlock()方法实现了对锁的显示控制,而synchronize()则是对锁的隐性控制。
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响,简单来说,该锁维护这一个与获取锁相关的计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,函数调用结束计数器就减1,然后锁需要被释放两次才能获得真正释放。已经获取锁的线程进入其他需要相同锁的同步代码块不会被阻塞。
-
import java.util.concurrent.locks.Condition;
-
import java.util.concurrent.locks.Lock;
-
import java.util.concurrent.locks.ReentrantLock;
-
/**
-
* 生产者和消费者,ReentrantLock的实现
-
*
-
* @author ZGJ
-
* @date 2017年6月22日
-
*/
-
public class Test2 {
-
private static Integer count = 0;
-
private static final Integer FULL = 10;
-
//创建一个锁对象
-
private Lock lock = new ReentrantLock();
-
//创建两个条件变量,一个为缓冲区非满,一个为缓冲区非空
-
private final Condition notFull = lock.newCondition();
-
private final Condition notEmpty = lock.newCondition();
-
public static void main(String[] args) {
-
Test2 test2 = new Test2();
-
new Thread(test2.new Producer()).start();
-
new Thread(test2.new Consumer()).start();
-
new Thread(test2.new Producer()).start();
-
new Thread(test2.new Consumer()).start();
-
new Thread(test2.new Producer()).start();
-
new Thread(test2.new Consumer()).start();
-
new Thread(test2.new Producer()).start();
-
new Thread(test2.new Consumer()).start();
-
}
-
class Producer implements Runnable {
-
@Override
-
public void run() {
-
for (int i = 0; i < 10; i++) {
-
try {
-
Thread.sleep(3000);
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
//获取锁
-
lock.lock();
-
try {
-
while (count == FULL) {
-
try {
-
notFull.await();
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
count++;
-
System.out.println(Thread.currentThread().getName()
-
+ "生产者生产,目前总共有" + count);
-
//唤醒消费者
-
notEmpty.signal();
-
} finally {
-
//释放锁
-
lock.unlock();
-
}
-
}
-
}
-
}
-
class Consumer implements Runnable {
-
@Override
-
public void run() {
-
for (int i = 0; i < 10; i++) {
-
try {
-
Thread.sleep(3000);
-
} catch (InterruptedException e1) {
-
e1.printStackTrace();
-
}
-
lock.lock();
-
try {
-
while (count == 0) {
-
try {
-
notEmpty.await();
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}
-
count--;
-
System.out.println(Thread.currentThread().getName()
-
+ "消费者消费,目前总共有" + count);
-
notFull.signal();
-
} finally {
-
lock.unlock();
-
}
-
}
-
}
-
}
-
}
阻塞队列BlockingQueue的实现
BlockingQueue是JDK5.0的新增内容,它是一个已经在内部实现了同步的队列,实现方式采用的是我们第2种await() / signal()方法。它可以在生成对象时指定容量大小,用于阻塞操作的是put()和take()方法。
put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞。
BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:
- 当队列满了的时候进行入队列操作
- 当队列空了的时候进行出队列操作
因此,当一个线程对已经满了的阻塞队列进行入队操作时会阻塞,除非有另外一个线程进行了出队操作,当一个线程对一个空的阻塞队列进行出队操作时也会阻塞,除非有另外一个线程进行了入队操作。
从上可知,阻塞队列是线程安全的。
下面是BlockingQueue接口的一些方法:
操作 | 抛异常 | 特定值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(o) | offer(o) | put(o) | offer(o, timeout, timeunit) |
移除 | remove(o) | poll(o) | take(o) | poll(timeout, timeunit) |
检查 | element(o) | peek(o) |
这四类方法分别对应的是:
1 . ThrowsException:如果操作不能马上进行,则抛出异常
2 . SpecialValue:如果操作不能马上进行,将会返回一个特殊的值,一般是true或者false
3 . Blocks:如果操作不能马上进行,操作会被阻塞
4 . TimesOut:如果操作不能马上进行,操作会被阻塞指定的时间,如果指定时间没执行,则返回一个特殊值,一般是true或者false
下面来看由阻塞队列实现的生产者消费者模型,这里我们使用take()和put()方法,这里生产者和生产者,消费者和消费者之间不存在同步,所以会出现连续生成和连续消费的现象
-
import java.util.concurrent.ArrayBlockingQueue;
-
import java.util.concurrent.BlockingQueue;
-
/**
-
* 使用BlockingQueue实现生产者消费者模型
-
* @author ZGJ
-
* @date 2017年6月29日
-
*/
-
public class Test3 {
-
private static Integer count = 0;
-
//创建一个阻塞队列
-
final BlockingQueue blockingQueue = new ArrayBlockingQueue<>(10);
-
public static void main(String[] args) {
-
Test3 test3 = new Test3();
-
new Thread(test3.new Producer()).start();
-
new Thread(test3.new Consumer()).start();
-
new Thread(test3.new Producer()).start();
-
new Thread(test3.new Consumer()).start();
-
new Thread(test3.new Producer()).start();
-
new Thread(test3.new Consumer()).start();
-
new Thread(test3.new Producer()).start();
-
new Thread(test3.new Consumer()).start();
-
}
-
class Producer implements Runnable {
-
@Override
-
public void run() {
-
for (int i = 0; i < 10; i++) {
-
try {
-
Thread.sleep(3000);
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
try {
-
blockingQueue.put(1);
-
count++;
-
System.out.println(Thread.currentThread().getName()
-
+ "生产者生产,目前总共有" + count);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
}
-
class Consumer implements Runnable {
-
@Override
-
public void run() {
-
for (int i = 0; i < 10; i++) {
-
try {
-
Thread.sleep(3000);
-
} catch (InterruptedException e1) {
-
e1.printStackTrace();
-
}
-
try {
-
blockingQueue.take();
-
count--;
-
System.out.println(Thread.currentThread().getName()
-
+ "消费者消费,目前总共有" + count);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
}
-
}
信号量Semaphore的实现
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源,在操作系统中是一个非常重要的问题,可以用来解决哲学家就餐问题。Java中的Semaphore维护了一个许可集,一开始先设定这个许可集的数量,可以使用acquire()方法获得一个许可,当许可不足时会被阻塞,release()添加一个许可。在下列代码中,还加入了另外一个mutex信号量,维护生产者消费者之间的同步关系,保证生产者和消费者之间的交替进行
-
import java.util.concurrent.Semaphore;
-
/**
-
* 使用semaphore信号量实现
-
* @author ZGJ
-
* @date 2017年6月29日
-
*/
-
public class Test4 {
-
private static Integer count = 0;
-
//创建三个信号量
-
final Semaphore notFull = new Semaphore(10);
-
final Semaphore notEmpty = new Semaphore(0);
-
final Semaphore mutex = new Semaphore(1);
-
public static void main(String[] args) {
-
Test4 test4 = new Test4();
-
new Thread(test4.new Producer()).start();
-
new Thread(test4.new Consumer()).start();
-
new Thread(test4.new Producer()).start();
-
new Thread(test4.new Consumer()).start();
-
new Thread(test4.new Producer()).start();
-
new Thread(test4.new Consumer()).start();
-
new Thread(test4.new Producer()).start();
-
new Thread(test4.new Consumer()).start();
-
}
-
class Producer implements Runnable {
-
@Override
-
public void run() {
-
for (int i = 0; i < 10; i++) {
-
try {
-
Thread.sleep(3000);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
try {
-
notFull.acquire();
-
mutex.acquire();
-
count++;
-
System.out.println(Thread.currentThread().getName()
-
+ "生产者生产,目前总共有" + count);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
} finally {
-
mutex.release();
-
notEmpty.release();
-
}
-
}
-
}
-
}
-
class Consumer implements Runnable {
-
@Override
-
public void run() {
-
for (int i = 0; i < 10; i++) {
-
try {
-
Thread.sleep(3000);
-
} catch (InterruptedException e1) {
-
e1.printStackTrace();
-
}
-
try {
-
notEmpty.acquire();
-
mutex.acquire();
-
count--;
-
System.out.println(Thread.currentThread().getName()
-
+ "消费者消费,目前总共有" + count);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
} finally {
-
mutex.release();
-
notFull.release();
-
}
-
}
-
}
-
}
-
}
管道输入输出流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();
-
}
-
}