多线程下的生产者与消费者模式及(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()

这也是传统的解决方案

步骤

  1. 给线程的任务方法加上synchronized关键字
  2. 条件休眠
  3. 相互唤醒
// 多线程下的生产者与消费者模式
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包提供的方式

代码和上述传统方案差不多

差别有以下俩点

  1. Lock替代synchronized,这里使用ReentrantLock(重入锁)
  2. condition.await()和condition.signal()代替wait()和notify(),Condition需要从锁中获取

步骤

  1. 创建Lock
  2. 获取Condition
  3. 给线程上锁
  4. 条件休眠
  5. 相互唤醒
  6. 释放线程的锁

代码如下

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() 的生产者/消费者模式

java多线程:生产者和消费者模式(wait-notify) : 单生产和单消费

线程通讯

Java多线程基础-第一节5:wait和notify

java 多线程-生产者消费者模式-管程法