生产者消费者
生产者-消费者,实际上包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中获取数据,不需要关心生产者的行为。如果共享数据区已满的话,阻塞生产者继续生产数据;如果共享数据区为空的话,阻塞消费者继续消费数据。
在实现生产者消费者问题时,可以采用三种方式:
- 使用 Object 的 wait/notify 的消息通知机制;
- 使用 Lock Condition 的 await/signal 消息通知机制;
- 使用 BlockingQueue 实现。
wait/notify 的消息通知机制
可以通过 Object 对象的 wait 方法和 notify 方法或 notifyAll 方法来实现线程间的通信。
调用 wait 方法将阻塞当前线程,直到其他线程调用了 notify 方法或 notifyAll 方法进行通知,当前线程才能从 wait 方法处返回,继续执行下面的操作。
-
wait:该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait 之前,线程必须获得该对象的监视器锁,即只能在同步方法或同步块中调用 wait 方法。调用 wait 方法之后,当前线程会释放锁。如果调用 wait 方法时,线程并未获取到锁的话,则会抛出 IllegalMonitorStateException异常。如果再次获取到锁的话,当前线程才能从 wait 方法处成功返回。
-
notify:该方法也需要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁,如果调用 notify 时没有持有适当的锁,也会抛出 IllegalMonitorStateException。该方法会从 WAITTING 状态的线程中挑选一个进行通知,使得调用 wait 方法的线程从等待队列移入到同步队列中,等待机会再一次获取到锁,从而使得调用 wait 方法的线程能够从 wait 方法处退出。调用 notify 后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁。
-
notifyAll:该方法与 notify 方法的工作方式相同,重要的一点差异是:notifyAll 会使所有原来在该对象上 wait 线程统统退出 WAITTING 状态,使得他们全部从等待队列中移入到同步队列中去,等待下一次获取到对象监视器锁的机会。
wait/notify 消息通知存在的问题
1. notify 过早通知
notify 通知的遗漏,即 threadA 还没开始 wait,threadB 已经 notify 了,这样,threadB 通知是没有任何响应的,当 threadB 退出 synchronized 代码块后,threadA 再开始 wait,便会一直阻塞等待,直到被别的线程打断。
public class Main {
public static void main(String[] args) {
String lockObject = "";
// 把对象作为锁
WaitThread waitThread = new WaitThread(lockObject);
NotifyThread notifyThread = new NotifyThread(lockObject);
// 先 notify 再 wait
notifyThread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitThread.start();
}
static class WaitThread extends Thread {
private final String lock;
public WaitThread(String lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " 进去代码块");
System.out.println(Thread.currentThread().getName() + " 开始wait");
lock.wait();
System.out.println(Thread.currentThread().getName() + " 结束wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class NotifyThread extends Thread {
private final String lock;
public NotifyThread(String lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 进去代码块");
System.out.println(Thread.currentThread().getName() + " 开始notify");
lock.notify();
System.out.println(Thread.currentThread().getName() + " 结束开始notify");
}
}
}
}
针对这种问题的解决方法是,添加一个状态标志,让 waitThread 调用 wait 方法前先判断状态是否已经改变了,如果通知已经发出,WaitThread 就不再去 wait。
public class Main {
private static boolean isWait = true;
public static void main(String[] args) {
String lockObject = "";
// 把对象作为锁
WaitThread waitThread = new WaitThread(lockObject);
NotifyThread notifyThread = new NotifyThread(lockObject);
// 先 notify 再 wait
notifyThread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitThread.start();
}
static class WaitThread extends Thread {
private final String lock;
public WaitThread(String lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
try {
while (isWait) {
System.out.println(Thread.currentThread().getName() + " 进去代码块");
System.out.println(Thread.currentThread().getName() + " 开始wait");
lock.wait();
System.out.println(Thread.currentThread().getName() + " 结束wait");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class NotifyThread extends Thread {
private final String lock;
public NotifyThread(String lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 进去代码块");
System.out.println(Thread.currentThread().getName() + " 开始notify");
lock.notify();
isWait = false;
System.out.println(Thread.currentThread().getName() + " 结束开始notify");
}
}
}
}
这段代码只增加了一个isWait
状态,NotifyThread 调用 notify 方法后会对状态进行更新,WaitThread 调用 wait 方法之前会先对状态进行判断。
该示例中,调用 notify 后将状态isWait
改变为 false,因此,在 WaitThread 中 while 对 isWait 判断后就不会执行 wait 方法,从而避免了 Notify 过早通知造成遗漏的情况。
总结:在使用线程的等待/通知机制时,一般都要配合一个 boolean 变量值,在 notify 之前改变该 boolean 变量的值,让 wait 返回后能够退出 while 循环,或在通知被遗漏后不会被阻塞在 wait 方法处。
2. 等待 wait 的条件发生变化
如果线程在等待时接收到了通知,但是之后等待的条件发生了变化,并没有再次对等待条件进行判断,也会导致程序出现错误。
public class Main {
private static final List<String> lockObject = new ArrayList<>();
public static void main(String[] args) {
Consumer consumer1 = new Consumer(lockObject);
consumer1.setName("consumer1");
Consumer consumer2 = new Consumer(lockObject);
consumer2.setName("consumer2");
Producer producer = new Producer(lockObject);
producer.setName("producer");
consumer1.start();
consumer2.start();
producer.start();
}
static class Consumer extends Thread {
private final List<String> lock;
public Consumer(List lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
try {
// 这里使用if的话,就会存在wait条件变化造成程序错误的问题
if (lock.isEmpty()) {
System.out.println(Thread.currentThread().getName() + " list为空");
System.out.println(Thread.currentThread().getName() + " 调用wait方法");
lock.wait();
System.out.println(Thread.currentThread().getName() + " wait方法结束");
}
String element = lock.remove(0);
System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Producer extends Thread {
private final List<String> lock;
public Producer(List lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 开始添加元素");
lock.add(Thread.currentThread().getName());
lock.notifyAll();
}
}
}
}
consumer1 list为空
consumer1 调用wait方法
producer 开始添加元素
consumer2 取出第一个元素为:producer
consumer1 wait方法结束
Exception in thread "consumer1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
Consumer1 从wait 方法退出后,想要取出列表首元素,但已经被 Consumer2 取走了。Consumer1 报错是因为线程从 wait 方法退出之后没有对 wait 条件进行判断,但此时的 wait 条件已经发生了变化。
只要将 wait 外围的 if 语句改为了 while 循环,这样当 list 为空时,线程便会继续等待,而不会继续去执行删除 list 中元素中的代码。
在使用线程的等待/通知机制时,一般都要在 while 循环中调用 wait 方法,因此需要配合一个 boolean 变量,满足 while 循环的条件时进入 while 循环,执行 wait 方法,不满足 while 循环条件时,跳出循环,执行后面的代码。
3. 假死
现象:如果是多消费者和多生产者情况,使用 notify 方法可能会出现“假死”的情况,即所有的线程都处于等待状态,无法被唤醒。
原因分析:假设当前有多个生产者线程调用了 wait 方法阻塞等待,其中一个生产者线程获取到对象锁之后使用 notify 通知处于 WAITTING 状态的线程,如果唤醒的仍然是生产者线程,就会造成所有的生产者线程都处于等待状态。
解决办法:将 notify 方法替换成 notifyAll 方法,如果使用的是 lock 的话,就将 signal 方法替换成 signalAll 方法。
使用规范
Object 提供的消息通知机制应该遵循如下这些条件:
- 永远在 while 循环中对条件进行判断而不是在 if 语句中进行 wait 条件的判断;
- 使用 NotifyAll 而不是使用 notify。
基本的使用范式如下:
// The standard idiom for calling the wait method in Java
synchronized (sharedObject) {
while (condition) {
sharedObject.wait();
// (Releases lock, and reacquires on wakeup)
}
// do action based upon condition e.g. take or put into queue
}
wait/notifyAll 实现生产者-消费者
public class Main {
public static void main(String[] args) {
List<Integer> linkedList = new LinkedList<>();
ExecutorService service = Executors.newFixedThreadPool(15);
for (int i = 0; i < 5; i++)
service.submit(new Producer(linkedList, 8));
for (int i = 0; i < 10; i++)
service.submit(new Consumer(linkedList));
}
static class Producer implements Runnable {
private final List<Integer> list;
private final int maxLength;
public Producer(List<Integer> list, int maxLength) {
this.list = list;
this.maxLength = maxLength;
}
@Override
public void run() {
while (true) {
// 对 list 对象上锁
synchronized (list) {
try {
while (list.size() == maxLength) {
System.out.println("生产者" + Thread.currentThread().getName() + " list已达到最大容量,进行wait");
list.wait();
System.out.println("生产者" + Thread.currentThread().getName() + " 退出wait");
}
Random random = new Random();
int i = random.nextInt();
System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + i);
list.add(i);
// 唤醒所有
list.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class Consumer implements Runnable {
private final List<Integer> list;
public Consumer(List<Integer> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
try {
while (list.isEmpty()) {
System.out.println("消费者" + Thread.currentThread().getName() + " list为空,进行wait");
list.wait();
System.out.println("消费者" + Thread.currentThread().getName() + " 退出wait");
}
Integer element = list.remove(0);
System.out.println("消费者" + Thread.currentThread().getName() + " 消费数据:" + element);
list.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
await/signal 消息通知机制
参照 Object 的 wait 和 notify/notifyAll 方法,Condition 也提供了同样的方法,即 await 方法和 signal/signalAll 方法。
public class Main {
private static final Logger log = Logger.getLogger(Main.class);
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition full = lock.newCondition();
private static final Condition empty = lock.newCondition();
public static void main(String[] args) {
List<Integer> linkedList = new LinkedList<>();
ExecutorService service = Executors.newFixedThreadPool(15);
for (int i = 0; i < 5; i++)
service.submit(new Producer(linkedList, 8, lock));
for (int i = 0; i < 10; i++)
service.submit(new Consumer(linkedList, lock));
}
static class Producer implements Runnable {
private final List<Integer> list;
private final int maxLength;
private final Lock lock;
public Producer(List<Integer> list, int maxLength, Lock lock) {
this.list = list;
this.maxLength = maxLength;
this.lock = lock;
}
@Override
public void run() {
while (true) {
lock.lock();
try {
while (list.size() == maxLength) {
System.out.println("生产者" + Thread.currentThread().getName() + " list已达到最大容量,进行wait");
full.await();
System.out.println("生产者" + Thread.currentThread().getName() + " 退出wait");
}
Random random = new Random();
int i = random.nextInt();
System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + i);
list.add(i);
empty.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
static class Consumer implements Runnable {
private final List<Integer> list;
private final Lock lock;
public Consumer(List<Integer> list, Lock lock) {
this.list = list;
this.lock = lock;
}
@Override
public void run() {
while (true) {
lock.lock();
try {
while (list.isEmpty()) {
System.out.println("消费者" + Thread.currentThread().getName() + " list为空,进行wait");
empty.await();
System.out.println("消费者" + Thread.currentThread().getName() + " 退出wait");
}
Integer element = list.remove(0);
System.out.println("消费者" + Thread.currentThread().getName() + " 消费数据:" + element);
full.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
}
BlockingQueue 实现
BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
有了这个队列,生产者就只需要关注生产,而不用管消费者的消费行为,更不用等待消费者线程执行完;消费者也只管消费,不用管生产者是怎么生产的,更不用等着生产者生产。
public class Main {
private static final LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(15);
for (int i = 0; i < 5; i++)
service.submit(new Producer(queue));
for (int i = 0; i < 10; i++)
service.submit(new Consumer(queue));
}
static class Producer implements Runnable {
private final BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
Random random = new Random();
int i = random.nextInt();
System.out.println("生产者" + Thread.currentThread().getName() + "生产数据" + i);
queue.put(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Consumer implements Runnable {
private final BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
Integer element = queue.take();
System.out.println("消费者" + Thread.currentThread().getName() + "正在消费数据" + element);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
总结
生产者-消费者模式一般用于将生产数据的一方和消费数据的一方分割开来,将生产数据与消费数据的过程解耦开来。
生产者-消费者模式的优点:
- 解耦:将生产者类和消费者类进行解耦,消除代码之间的依赖性,简化工作负载的管理
- 复用:通过将生产者类和消费者类独立开来,对生产者类和消费者类进行独立的复用与扩展
- 调整并发数:由于生产者和消费者的处理速度是不一样的,可以调整并发数,给予慢的一方多的并发数,来提高任务的处理速度
- 异步:对于生产者和消费者来说能够各司其职,生产者只需要关心缓冲区是否还有数据,不需要等待消费者处理完;对于消费者来说,也只需要关注缓冲区的内容,不需要关注生产者,通过异步的方式支持高并发,将一个耗时的流程拆成生产和消费两个阶段,这样生产者因为执行 put 的时间比较短,可以支持高并发
- 支持分布式:生产者和消费者通过队列进行通讯,所以不需要运行在同一台机器上,在分布式环境中可以通过 redis 的 list 作为队列,而消费者只需要轮询队列中是否有数据。同时还能支持集群的伸缩性,当某台机器宕掉的时候,不会导致整个集群宕掉