一、生产者与消费者
所谓生产者与消费者模式就是通过平衡生产线程与消费线程的工作能力来提高程序整体处理数据的速度…
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完才能继续生产数据,反之亦然,为了解决这种生产消费能力不均衡的问题,便有了生产者消费者模式。
二、利用通知机制实现生产者消费者模型
下面先从一个简单的例子入手:
package multithread;
public class ProduceAndConsume {
private boolean isProduce = false;
private final Object LOCK = new Object();
private volatile int i = 0;
public void produce() {
synchronized (LOCK) {
if (isProduce) {
try {
LOCK.wait();
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("produce => " + ++i);
isProduce = true;
LOCK.notify();
}
}
}
public void consume() {
synchronized (LOCK) {
if (!isProduce) {
try {
LOCK.wait();
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("consume => " + i);
isProduce = false;
LOCK.notify();
}
}
}
public static void main(String[] args) {
ProduceAndConsume pc = new ProduceAndConsume();
new Thread(() -> {
while (true) {
pc.produce();
}
}, "produce").start();
new Thread(() -> {
while (true) {
pc.consume();
}
}, "consume").start();
}
}
在上面的例子中我们分别启动了一个生产线程和一个消费线程,实现了一个最简单的生产者消费者模型,从截图中可以看出来,生产线程总是先生产一个数据,消费线程再去消费线程。但这里只是一个生产线程和一个消费线程,如果是多个线程这样的写法还行不行呢?下面改动实践一下:
public static void main(String[] args) {
ProduceAndConsume pc = new ProduceAndConsume();
Stream.of("P1","P2").forEach( n -> {
new Thread(() -> {
while (true) {
pc.produce();
}
}, "produce").start();
});
Stream.of("C1","C2").forEach(n -> {
new Thread(() -> {
while (true) {
pc.consume();
}
}, "consume").start();
});
}
在这个我们分别用两个生产线程和两个消费线程,结果是生产消费了两次后就 block 住了,具体是什么原因呢?
根本原因是是 notify()在唤醒线程的时候唤醒的具体是哪个线程不能确定;那么这里的流程就有可能为: P1 线程生产一个数据以后进入 wait 状态,C1 线程消费一个数据之后唤醒 P1 线程并进入 wait 状态,之后 P2 线程生产一个数据之后唤醒 P1 线程并进入wait 状态,最后 C2 消费一个数据之后唤醒 P1 线程并进入 wait状态, P1 生产一个数据后唤醒了 P2,P2 被唤醒之后又进入 wait 状态。
最后第三个版本修改如下:
package multithread;
import java.util.stream.Stream;
public class ProduceAndConsume {
private boolean isProduce = false;
private final Object LOCK = new Object();
private volatile int i = 0;
public void produce() {
synchronized (LOCK) {
while (isProduce) {
try {
LOCK.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("produce => " + ++i);
isProduce = true;
LOCK.notifyAll();
}
}
public void consume() {
synchronized (LOCK) {
while (!isProduce) {
try {
LOCK.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("consume => " + i);
isProduce = false;
LOCK.notifyAll();
}
}
public static void main(String[] args) {
ProduceAndConsume pc = new ProduceAndConsume();
Stream.of("P1","P2").forEach( n -> {
new Thread(() -> {
while (true) {
pc.produce();
}
}, "produce").start();
});
Stream.of("C1","C2").forEach(n -> {
new Thread(() -> {
while (true) {
pc.consume();
}
}, "consume").start();
});
}
}
这里就是把 notify() 改为 notifyAll(),if 改为 while;改成 while 的原因是防止多个生产线程或者多个消费线程进入 wait 状态,每次只有一个线程可以获得监视器对象锁。
三、利用阻塞队列实现生产者消费者模型
生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是从阻塞队列里面取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
实际上,阻塞队列也是通过使用通知模式来实现通信的,用 Lock 锁的多条件(Condition)阻塞控制。使用 BlockingQueue 封装了根据条件阻塞线程的过程,而我们就不用关心繁琐的 await / signal 操作了。
package multithread;
import java.util.concurrent.LinkedBlockingQueue;
public class ProduceAndConsume2 {
private static LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(2);
public static class Produce extends Thread{
@Override
public void run() {
try {
queue.put("product");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("produce...");
}
}
public static class Consume extends Thread{
@Override
public void run() {
try {
String product = queue.take();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("consume...");
}
}
public static void main(String[] args) {
for (int i = 0; i < 15; i++) {
Produce produce = new Produce();
produce.start();
}
for (int i = 0; i < 15; i++) {
Consume consume = new Consume();
consume.start();
}
}
}
四、线程池与生产者消费者模式
Java 中的线程池其实就是一种生产者和消费者模式的实现,生产者把任务丢给线程池,线程池创建处理任务,如果将要运行的任务大于线程池的核心线程数,那么就将任务扔到阻塞队列里,这种做法比只使用一个阻塞队列来实现生产者与消费者模式显然要高明得多。