多线程下的生产者与消费者模式及(notify()与signal()唤醒的使用和区别)
Posted 若曦`
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程下的生产者与消费者模式及(notify()与signal()唤醒的使用和区别)相关的知识,希望对你有一定的参考价值。
1. 问题的起源
用现实中的快餐店:生产一个汉堡消费一个汉堡为例
代码示例
// 多线程下的生产者与消费者模式
public class bounded_buffer_Problem {
public static void main(String[] args) {
HamburgerShop hamburgerShop = new HamburgerShop();
// 开启一个生产者线程,for循环1000次,生产1000个汉堡
new Thread(()->{
for (int i = 0; i < 1000; i++) {
hamburgerShop.produce();
}
},"生产者线程").start();
// 开启一个消费者线程
new Thread(()->{
for (int i = 0; i < 1000; i++) {
hamburgerShop.consume();
}
},"消费者").start();
}
}
// 汉堡店类
class HamburgerShop{
// 剩下的数量
private Integer num=0;
// 生产汉堡
// 使用synchronized关键字,防止多个生产者一起生产导致数据出错
public synchronized void produce(){
num++;
System.out.println(Thread.currentThread().getName()+"++++生产了一个汉堡,还剩下"+num+"个");
}
//消费汉堡
public synchronized void consume(){
num--;
System.out.println(Thread.currentThread().getName()+"----消费了一个汉堡,还剩下"+num+"个");
}
}
这时候运行代码,会出现如下情况
为了解决上图问题,我们需要在生产或消费中,暂停该线程
比如消费时,需要判断汉堡数量是否为0,如果没有汉堡则不能继续消费了
这时候可以采用synchronized+wait()+notify()的方式,去暂停和唤醒线程
2. 传统的解决方案
(1) if()造成的虚假唤醒问题
使用wait()休眠当前对象,需要使用while循环去判断,不能使用if(num==0)这样的判断条件,因为 if()会存在线程虚假唤醒的问题,if()造成的虚假唤醒是因为,if()的判断条件只会执行一次,而while()每次都会先判断循环条件,所以 if 不是真正的唤醒,需要使用 while 完成多重检测,避免这一问题。
一般2个线程交替执行,不会出现这样的问题,但是超过2个线程执行,就常常会出现这样的虚假唤醒问题
(2) 生产消费问题的解决
① synchronized+wait()+notify()
这也是传统的解决方案
步骤
- 给线程的任务方法加上synchronized关键字
- 条件休眠
- 相互唤醒
// 多线程下的生产者与消费者模式
public class bounded_buffer_Problem {
public static void main(String[] args) {
HamburgerShop hamburgerShop = new HamburgerShop();
// 开启一个生产者线程,for循环10次,生产10个汉堡
new Thread(()->{
for (int i = 0; i < 10; i++) {
hamburgerShop.produce();
}
},"生产者线程").start();
// 开启一个消费者线程,也是循环10次
new Thread(()->{
for (int i = 0; i < 10; i++) {
hamburgerShop.consume();
}
},"消费者").start();
}
}
// 汉堡店类
class HamburgerShop{
// 剩下的数量
private Integer num=0;
// 生产汉堡
//1. 线程的任务方法添加synchronized关键字
public synchronized void produce(){
while (num>0){ //如果有做好的汉堡,则暂停生产者线程
try {
//2. 条件休眠
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
//3. 相互唤醒
this.notify(); // 唤醒被阻塞的消费者线程
//notifyAll();
System.out.println(Thread.currentThread().getName()+"++++生产了一个汉堡,还剩下"+num+"个");
}
//消费汉堡
// 1. 线程的任务方法添加synchronized关键字
public synchronized void consume() {
while (num == 0){ //如果没有汉堡,则暂停消费者线程
// 暂停线程
try {
//2. 条件休眠
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
//3. 相互唤醒
this.notify(); // 唤醒被阻塞的生产者线程
//notifyAll();
System.out.println(Thread.currentThread().getName()+"----消费了一个汉堡,还剩下"+num+"个");
}
}
为什么不用sleep()?
因为sleep是休眠线程对象,而wait()是休眠资源对象这是主要原因,而且sleep是指定一段时间内休眠,休眠结束不一定会已经生产出汉堡
② Lock+Condition
Lock+Condition是JUC包提供的方式
代码和上述传统方案差不多
差别有以下俩点
- Lock替代synchronized,这里使用ReentrantLock(重入锁)
- condition.await()和condition.signal()代替wait()和notify(),Condition需要从锁中获取
步骤
- 创建Lock
- 获取Condition
- 给线程上锁
- 条件休眠
- 相互唤醒
- 释放线程的锁
代码如下
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 多线程下的生产者与消费者模式
public class bounded_buffer_Problem {
public static void main(String[] args) {
HamburgerShop hamburgerShop = new HamburgerShop();
// 开启一个生产者线程,for循环10次,生产10个汉堡
new Thread(()->{
for (int i = 0; i < 10; i++) {
hamburgerShop.produce();
}
},"生产者线程").start();
// 开启一个消费者线程,也是循环10次
new Thread(()->{
for (int i = 0; i < 10; i++) {
hamburgerShop.consume();
}
},"消费者").start();
}
}
// 汉堡店类
class HamburgerShop{
// 1.创建一把重入锁
private Lock lock = new ReentrantLock();
// 2. 获取锁的Condition
private Condition condition = lock.newCondition();
// 剩下的数量
private Integer num=0;
// 生产汉堡
public void produce(){
// 3. 上锁
lock.lock();
while (num>0){ //如果有做好的汉堡,则不再继续生产了
try {
condition.await(); // 4. 使生产者线程暂停
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
condition.signal(); // 5. 唤醒被阻塞的消费者线程
//condition.signalAll();
System.out.println(Thread.currentThread().getName()+"++++生产了一个汉堡,还剩下"+num+"个");
// 6. 释放锁
lock.unlock();
}
//消费汉堡
public void consume() {
// 3. 上锁
lock.lock();
while (num == 0){ //如果没有汉堡,则暂停消费者线程
// 暂停线程
try {
// 4. 使消费者线程暂停
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
condition.signal(); // 5. 唤醒被阻塞的生产者线程
//condition.signalAll();
System.out.println(Thread.currentThread().getName()+"----消费了一个汉堡,还剩下"+num+"个");
// 6. 释放锁
lock.unlock();
}
}
3. notify()到底唤醒的是谁?
一句话总结 : notify()是随机唤醒一个被其拥有的锁所阻塞的线程中的对象(可以看作随机唤醒一个wait()方法)
当wait()、notify/notifyAll() 方法在synchronized 所修饰的代码块内执行,说明当前线程一定获取了锁(synchronized的内置锁)
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态
当 notify/notifyAll() 被执行的时候,会唤醒当前锁所阻塞的线程中的对象,继续执行该线程的任务
wait()和notify()合作使用,可以看作是实现了线程间的通信
用下例代码来说明
执行上述代码来看结果
可以看出,生产者执行了2次,而消费者执行了一次,没有一直执行和消费下去,这是为什么呢?
因为消费者使用了notify()唤醒了生产者,而生产者没有调用notify()唤醒消费者
解释说明代码流程
1. 生产者执行,此时num=0,那么开始生产,执行num++,打印输出生产了一个汉堡,任务结束
2. 这时第二个生产者来了,发现此时num>0,有存货,那么开始wait(),进入休眠,并释放掉对象的锁,那么此时消费者拿到了锁,开始执行消费
3. 消费者开始执行,此时num=1,也就是有汉堡,执行num--(消费),接着notify()唤醒生产者线程,提醒没汉堡了,赶快生产,然后打印输出消费了一个汉堡,任务结束
4. 第二个生产者被唤醒,此时num=0,那么开始生产,num++; 打印输出生产了一个汉堡,任务结束
5. 这时第三个生产者来了,发现此时num>0,有存货,那么开始wait(),进入休眠,并释放掉对象的锁,那么此时消费者拿到了锁,但这时消费者不会执行,这是因为消费者还在wait()休眠状态,没有任何一个线程去唤醒他
所以这时消费者没有再执行,程序也就卡在了这一步
所以要解决这个问题,也就是在生产者中也要使用notify()去唤醒消费者,也就是生产者和消费者相互唤醒
或者是直接都使用notifyAll(),直接唤醒所有被阻塞的线程(如果判断不清谁唤醒谁,则可以使用这种方案)
4. Condition的精准唤醒
Condition是用来搭配Lock使用的,相当于是Lock和线程的一个监听器,可以用于实现Lock方式的线程休眠与唤醒
Condition中的休眠等待是await()方法,该方法和Object的wait()方法类似,会使当前线程在该语句停下等待,同样会释放当前线程所拥有的锁
Condition中的signal()唤醒方法,可以通过指定具体的一个condition对象,去精准唤醒一个线程
精准唤醒的实现:1个Lock,多个Condition,不同的Condition管理不同的对象
利用精准唤醒,实现A唤醒B,B唤醒C,C唤醒A
代码示例
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ABC {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printC();
}
},"C").start();
}
}
class Data{
//一把锁,多个Condition,不同的condition管理不同的线程任务
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private static int number = 1;
public void printA(){
lock.lock();
while(number!=1){
try {
//condition1管理线程A
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("==="+Thread.currentThread().getName());
number=2;
//唤醒condition2管理的线程
condition2.signal();
lock.unlock();
}
public void printB(){
lock.lock();
while(number!=2){
try {
//condition2管理线程B
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("==="+Thread.currentThread().getName());
number=3;
//唤醒condition3管理的线程
condition3.signal();
lock.unlock();
}
public void printC(){
lock.lock();
while(number!=3){
try {
//condition3管理线程C
condition3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("==="+Thread.currentThread().getName());
System.out.println("---------");
number=1;
//唤醒condition1管理的线程
condition1.signal();
lock.unlock();
}
}
5. notify()与signal()唤醒的区别
notify()和signal()都是唤醒一个被当前锁所阻塞的一个线程中的对象,也就是对于的wait()或await()语句
而notify()是随机唤醒一个线程,signal()可以实现精准唤醒
在只有一个Condition监视器管理线程的时候,notify()和signal()方法使用完全相同
但是有多个Condition的时候,signal()可以实现一个线程的精准唤醒
以上是关于多线程下的生产者与消费者模式及(notify()与signal()唤醒的使用和区别)的主要内容,如果未能解决你的问题,请参考以下文章
多线程-并发编程-生产者消费者模式及非阻塞队列与阻塞队列实现
java多线程15 :wait()和notify() 的生产者/消费者模式