生产者和消费者模式概述 [应用]
-
概述
1.生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们 对多线程编程的理解更加深刻。
2.所谓生产者消费者问题,实际上主要是包含了两类线程:
一类是生产者线程用于生产数据
一类是消费者线程用于消费数据
3. 为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
-
Object类的等待和唤醒方法
方法名 说明 void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 void notify() 唤醒正在等待对象监视器的单个线程 void notifyAll() 唤醒正在等待对象监视器的所有线程
- 要使用等待和唤醒还需到搭配synchronized关键字一起使用!
- 必须用锁对象去调用wait()方法和notifyAll()方法!
- 锁对象.wait():表示让当前线程跟这把锁进行绑定!
- 锁对象.notifyAll():表示现在要去唤醒,绑定在这把锁上的所有线程!
- 生产者和消费者,也叫做等待唤醒机制,它是一种十分经典的多线程协作的模式。
- 线程的执行,它是有随机性的,等待唤醒机制,这个机制,就要打破随机的规则,它会让两个线程轮流执行,你一次我一次,你一次我一次,这就是在等待唤醒机制中,多线程的运行结果。
- 其中的一条线程,我们把它叫做生产者,负责生产数据
- 另一条线程,叫做消费者,负责消费数据
- 还需要有第三者桌子,因为线程的执行是具有随机性的,还需要有一个东西去控制线程的执行。
- 如果桌子上有吃的,消费者负责吃;如果桌子上没有吃的,生产者负责做。
- 有两种情况,消费者等待和生产者等待。吃货在等:wait;厨师要去唤醒:notify
等待唤醒机制:消费者和生产者代码实现
package com.gch.d11_wait_and_notify;
/**
作用:控制生产者和消费者的执行
*/
public class Desk {
// 是否有面条 0:没有面条 1:有面条
// boolean:true false(只有两个值,只能控制两条线程的执行)
public static int foodFlag = 0;
// 总个数 表示吃货最多可以吃十碗
public static int count = 10;
// 锁对象
public static final Object lock = new Object();
}
package com.gch.d11_wait_and_notify;
/**
生产者:表示厨师
*/
public class Producer extends Thread {
/**
* 调用父类的有参构造器
* @param name:线程名
*/
public Producer(String name){
super(name);
}
/*
1.循环
2.同步代码块
3.判断共享数据是否已经到了末尾(到了末尾)
4.判断共享数据是否已经到了末尾(没有到末尾,执行核心逻辑)
*/
@Override
public void run() {
// 1.循环
while(true){
// 2.同步代码块
synchronized(Desk.lock){
// 3.判断共享数据是否已经到了末尾(到了末尾)
if(Desk.count == 0){
break;
}else{
// 4.判断共享数据是否已经到了末尾(没有到末尾,执行核心逻辑)
// 判断桌子上是否有食物
if(Desk.foodFlag == 1){
try {
// 如果有,就等待
Desk.lock.wait(); // 让当前线程跟锁进行绑定起来
} catch (Exception e) {
e.printStackTrace();
}
}else{
// 如果没有,就制作食物
System.out.println("厨师做了一碗面条!");
// 修改桌子上的食物状态
Desk.foodFlag = 1;
// 叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
package com.gch.d11_wait_and_notify;
/**
消费者:表示吃货
*/
public class Consumer extends Thread {
/**
* 调用父类的有参构造器
* @param name:线程名
*/
public Consumer(String name){
super(name);
}
@Override
public void run() {
/*
1.循环
2.同步代码块
3.判断共享数据是否到了末尾(到了末尾)
4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
*/
// 1.循环
while(true){
// 2.同步代码块
synchronized(Desk.lock){
// 3.判断共享数据是否已经到了末尾(到了末尾)
if(Desk.count == 0){
break; // 循环一旦停止,线程就要结束了
}else{ // 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
// 先判断桌子上是否有面条
if(Desk.foodFlag == 0){
try {
// 如果没有,就等待
// 必须用锁对象去调用wait方法和notifyAll()方法
// Desk.lock.notifyAll(); // 唤醒跟这把锁绑定的所有线程
Desk.lock.wait(); // 让当前线程跟锁进行绑定
} catch (Exception e) {
e.printStackTrace();
}
}else{ // 如果有,就开吃
// 把吃的总数 - 1
Desk.count--;
// 如果有,就开吃
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!");
// 吃完之后,唤醒厨师继续做
Desk.lock.notifyAll(); // 表示现在要去唤醒,绑定在这把锁上的所有线程
// 修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
package com.gch.d11_wait_and_notify;
public class ThreadDemo {
public static void main(String[] args) {
/*
需求:完成生产者和消费者(等待唤醒机制)的代码
实现线程轮流交替执行的效果
*/
// 1.创建线程的对象
Thread p = new Producer("厨师");
Thread c = new Consumer("吃货");
// 2.开启线程
p.start();
c.start();
}
}
二. 阻塞队列实现等待唤醒机制
- 阻塞队列:连接生产者与消费者之间的管道。
- 阻塞队列实现了4个接口
阻塞队列由于实现了Collection接口,所以阻塞队列就是一个单列集合。
阻塞队列可以利用迭代器或者增强for循环来遍历。
生产者线程和消费者线程必须使用同一个阻塞队列!
有界表示是有长度的界限。 因此****在创建ArrayBlockingQueue对象的时候,必须要去指定队列的最大长度。
无界指的是没有长度的界限。
常见BlockingQueue(阻塞队列):
ArrayBlockingQueue : 底层是数组,有界队列
LinkedBlockingQueue : 底层是链表,无界,它底层是一个有界队列的实现方式(传参指定队列大小)=>但不是真正的无界,看源码:如果没有传参的话,最大为Integer(int)的最大值:
BlockingQueue的核心方法:
- put(anObject): 将参数放入队列,如果放不进去会阻塞
- take(): 取出第一个数据,取不到会阻塞
package com.gch.d12_wait_and_notify;
import java.util.concurrent.ArrayBlockingQueue;
/**
生产者:表示厨师
*/
public class Producer extends Thread {
public ArrayBlockingQueue<String> queue;
/**
* 有参构造器
* @param name:线程名
* @param queue:阻塞队列
*/
public Producer(String name, ArrayBlockingQueue<String> queue) {
super(name);
this.queue = queue;
}
@Override
public void run() {
while(true){
try {
// 1.不断的把面条放到阻塞队列当中
queue.put("面条"); // put方法底层源码就已经使用了Lock锁
System.out.println(getName() + "放了一碗面条!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
package com.gch.d12_wait_and_notify;
import java.util.concurrent.ArrayBlockingQueue;
/**
* 消费者:表示吃货
*/
public class Consumer extends Thread {
public ArrayBlockingQueue<String> queue;
/**
* 有参构造器
* @param name:线程名
* @param queue:阻塞队列
*/
public Consumer(String name, ArrayBlockingQueue<String> queue) {
super(name);
this.queue = queue;
}
@Override
public void run() {
while(true){
// 1.不断的从阻塞队列中获取面条
String food = null;
try {
food = queue.take(); // take方法的底层也是有Lock锁的
System.out.println(food);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
package com.gch.d12_wait_and_notify;
import java.util.concurrent.ArrayBlockingQueue;
public class ThreadDemo {
public static void main(String[] args) {
/*
需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
细节:
生产者和消费者必须使用同一个阻塞队列
*/
// 1.创建阻塞队列的对象
// ArrayBlockingQueue是有界阻塞队列,在创建ArrayBlockingQueue对象的时候必须指定它的上限
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
// 2.创建线程的对象,并把阻塞队列传递过去
Thread p = new Producer("厨师",queue);
Thread c = new Consumer("吃货",queue);
// 3.开启线程
p.start();
c.start();
}
}
三. Condition类的await(),signal()和signalAll()实现线程通信
package com.gch.d20_wait_notify;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
线程间的通信-生产者消费者模式-等待唤醒机制
线程间通信的第二种方式:ReentrantLock + 上锁 + 解锁 + 标记位状态 + Condition + signal() + await()
使用ReentrantLock可以实现精准唤醒某一个线程
Reentrant lock = new ReentrantLock();
Condition接口的await()等待,signal()唤醒和signAll()实现线程通信
Condition condition = lock.newCondition();
condition.await(); 让自身线程等待
condition.signal(); 唤醒具体的某个线程
线程它本身是一个争抢CPU的机制,所以它们出牌的顺序肯定是乱的。
总结:当有多个线程需要按照一定的顺序条件执行的时候,或者要等待别的线程执行到某种状态之后,才能让其他的特定的线程执行的话,想到这套方案
*/
public class ThreadDemo {
// 记录三个玩家各自出牌的总数
public static int sumPlayer1 = 0; // 玩家1
public static int sumPlayer2 = 0; // 玩家2
public static int sumPlayer3 = 0; // 玩家3
// 标记位 表示该谁出牌 1:玩家1出牌 2:玩家2出牌 3:玩家3出牌
public static int playerStatus = 1;
// 给每位玩家都返回一个Condition对象,准备让自身线程等待和唤醒其他线程的
public static Condition condition1;
public static Condition condition2;
public static Condition condition3;
// 锁对象,唯一的不可替换的
public static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws Exception{
// 1.给每位玩家都返回一个Condition对象,准备让自身线程等待和唤醒其他线程的
condition1 = lock.newCondition(); // 玩家1
condition2 = lock.newCondition(); // 玩家2
condition3 = lock.newCondition(); // 玩家3
// 2.创建线程对象
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
method1();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
},"玩家1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
method2();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
},"玩家2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
method3();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
},"玩家3");
// 3.开启线程
t1.start();
t2.start();
t3.start();
}
/**
玩家1
*/
public static void method1() throws Exception {
// 1.循环
while(true){
// 2.上锁
lock.lock();
try{
// 3.判断是哪位玩家出牌
if(playerStatus == 1){ // 玩家1出牌
int number = (int)(Math.random() * 17 + 1); // 每次出1-17张牌
sumPlayer1 += number;
// 4.判断共享数据释放已经到了末尾(到了末尾)
if(sumPlayer1 >= 17){
System.out.println(Thread.currentThread().getName() + "总共出了" + sumPlayer1+ "张牌,赢了这局");
break;
}else{
// 4.判断共享数据是否已经到了末尾(没有到末尾,执行核心逻辑)
System.out.println(Thread.currentThread().getName() + "出了" + number + "张牌");
playerStatus = 2; // 改变用户出牌状态为玩家2
condition2.signal(); // 唤醒玩家2出牌
}
}else{
// 不是玩家1出牌,等待
condition1.await();
}
}finally {
lock.unlock(); // 解锁 / 释放锁
}
}
}
/**
玩家2
*/
public static void method2() throws InterruptedException {
// 1.循环
while(true){
// 2.上锁
lock.lock();
try{
// 3.判断是哪位玩家出牌
if(playerStatus == 2){ // 玩家2出牌
int number = (int)(Math.random() * 17 + 1); // 每次出1-17张牌
sumPlayer2 += number;
// 4.判断共享数据释放已经到了末尾(到了末尾)
if(sumPlayer2 >= 17){
System.out.println(Thread.currentThread().getName() + "总共出了" + sumPlayer2+ "张牌,赢了这局");
break;
}else{
// 4.判断共享数据是否已经到了末尾(没有到末尾,执行核心逻辑)
System.out.println(Thread.currentThread().getName() + "出了" + number + "张牌");
playerStatus = 3; // 改变用户出牌状态为玩家2
condition3.signal(); // 唤醒玩家3出牌
}
}else{
// 不是玩家2出牌,等待
condition2.await();
}
}finally {
lock.unlock(); // 解锁 / 释放锁
}
}
}
/**
玩家3
*/
public static void method3() throws InterruptedException {
// 1.循环
while(true){
// 2.上锁
lock.lock();
try{
// 3.判断是哪位玩家出牌
if(playerStatus == 3){ // 玩家3出牌
int number = (int)(Math.random() * 17 + 1); // 每次出1-17张牌
sumPlayer3 += number;
// 4.判断共享数据释放已经到了末尾(到了末尾)
if(sumPlayer3 >= 17){
System.out.println(Thread.currentThread().getName() + "总共出了" + sumPlayer3+ "张牌,赢了这局");
break;
}else{
// 4.判断共享数据是否已经到了末尾(没有到末尾,执行核心逻辑)
System.out.println(Thread.currentThread().getName() + "出了" + number + "张牌");
playerStatus = 1; // 改变用户出牌状态为玩家1
condition1.signal(); // 唤醒玩家1出牌
}
}else{
// 不是玩家3出牌,等待
condition3.await();
}
}finally {
lock.unlock(); // 解锁 / 释放锁
}
}
}
}