JUC并发编程基础学习之生产者消费者问题

前言

面试常问:单例模式,排序算法,生产者和消费者问题,死锁问题

解决生产者消费者问题,我们可以使用synchronized或者Lock锁来解决,这就是我们今天所要学习和讨论的问题!

1.传统的生产者消费者问题

1.1 生产者消费者简单了解
1-1 什么是生产者消费者问题?

本质上是 线程之间的通信问题 (即等待唤醒通知唤醒)

例如线程交替执行, A、B操作同一个变量 num=0, A: num+1, B: num+1

1-2 生产者消费者实现步骤

生产者,消费者问题三步走

  1. 判断是否进入等待
  2. 执行相关业务
  3. 通知其他线程
1.2 生产者消费者问题具体实现

这里,我们将生产者和消费者问题转换成银行账户存钱和取钱问题,并进行两轮测试,第一轮 测试只存在AB两个线程,即A存钱,B取钱;第二轮测试存在ABCD四个线程,即A存钱,BCD取钱,分别观察各自执行结果如何

2-1 只存在AB两个线程

AB两个线程中,A负责存钱,而B负责取钱

  • 代码实现
package com.kuang.lock;

/**
 * @Description 生产者消费者问题
 * 只有AB两个线程  
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/23
 */

public class ProducerConsumerTest {

    public static void main(String[] args) {
        //获取Money资源类
        Money money = new Money();
        //A线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //存钱
                    money.saveMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小A").start();
        //B线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //取钱
                    money.drawMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小B").start();
    }
}

//资源类:Money
class Money {

   //钱的余额
   private int number = 0;

    //只要是并发编程就一定要有锁
   //存钱(使用synchronized关键字进行加锁)
    public synchronized void saveMoney() throws InterruptedException {
        if (number != 0) {
            //如果余额不为0,则该线程进入等待
            this.wait();
        }
        //否则余额加1
        number++;
        //打印存钱信息
        System.out.println(Thread.currentThread().getName()+"取出后,剩余"+number+"000元");
        //通知其他的线程,已经存好钱了
        this.notifyAll();
    }

   //取钱
   public synchronized void drawMoney() throws InterruptedException {
      //判断余额是否为0
      if (number == 0) {
          //如果余额为0,则该线程进行等待
          this.wait();
      }
      //否则余额减1
      number--;
       //打印存钱信息
       System.out.println(Thread.currentThread().getName()+"取出后,剩余"+number+"000元");
      //通知其他线程,已经取完钱了
       this.notifyAll();
   }
}

  • 测试结果:

    在这里插入图片描述

结果存入和取出后的余额正常,符合我们的预期!

2-2 同时存在ABCD四个线程

ABCD四个线程中,A负责存钱,而BCD负责取钱

  • 代码实现:
package com.kuang.lock;

/**
 * @Description 生产者消费者问题
 * 存在ABCD四个线程
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/23
 */

public class ProducerConsumerTest {

    public static void main(String[] args) {
        //获取Money资源类
        Money money = new Money();
        //A线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //存钱
                    money.saveMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小A").start();
        //B线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //取钱
                    money.drawMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小B").start();
        //C线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //取钱
                    money.drawMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小C").start();
        //D线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //取钱
                    money.drawMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小D").start();
    }
}

//资源类:Money
class Money {

   //钱的余额
   private int number = 0;

    //只要是并发编程就一定要有锁
   //存钱(使用synchronized关键字进行加锁)
    public synchronized void saveMoney() throws InterruptedException {
	 if (number != 0) {
            //如果余额不为0,则该线程进入等待
            this.wait();
        }
        //否则余额加1
        number++;
        //打印存钱信息
        System.out.println(Thread.currentThread().getName()+"存入"+number+"000元");
        //通知其他的线程,已经存好钱了
        this.notifyAll();
    }

   //取钱
   public synchronized void drawMoney() throws InterruptedException {
      //判断越是否为0
	 if (number == 0) {
          //如果余额为0,则该线程进行等待
          this.wait();
      }
      //否则余额减1
      number--;
       //打印存钱信息
       System.out.println(Thread.currentThread().getName()+"取出"+number+"000元");
      //通知其他线程,已经取完钱了
       this.notifyAll();
   }
}
  • 测试结果:

在这里插入图片描述

结果数据出现混乱,余额出现负数,这显然是不符合我们预期的!

结果分析

我们发现在C取钱后余额为零时B没有等待A存钱,仍然继续取钱,所以余额出现了负数,那么为什么会出现这种情况呢,这是因为出现了虚假唤醒

1.3 解决虚假唤醒问题
3-1 什么是虚假唤醒?

在JDK 8 的API文档中,是这样解释的

所谓虚假唤醒,就是线程可以被唤醒,而不会被通知、中断或者超时

在这里插入图片描述

解决方案

通过JDK 8 API 文档我们可以得知,等待应该总是出现在while循环中,因此我们可以将if判断修改为while循环

3-2 使用while循环解决虚假唤醒问题
  • 代码实现
package com.kuang.lock;

/**
 * @Description 生产者消费者问题
 * 存在ABCD四个线程
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/23
 */

public class ProducerConsumerTest {

    public static void main(String[] args) {
        //获取Money资源类
        Money money = new Money();
        //A线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //存钱
                    money.saveMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小A").start();
        //B线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //取钱
                    money.drawMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小B").start();
        //C线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //取钱
                    money.drawMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小C").start();
        //D线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //取钱
                    money.drawMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小D").start();
    }
}

//资源类:Money
class Money {

   //钱的余额
   private int number = 0;

    //只要是并发编程就一定要有锁
   //存钱(使用synchronized关键字进行加锁)
    public synchronized void saveMoney() throws InterruptedException {
	 while (number != 0) {
            //如果余额不为0,则该线程进入等待
            this.wait();
        }
        //否则余额加1
        number++;
        //打印存钱信息
        System.out.println(Thread.currentThread().getName()+"存入"+number+"000元");
        //通知其他的线程,已经存好钱了
        this.notifyAll();
    }

   //取钱
   public synchronized void drawMoney() throws InterruptedException {
      //判断余额是否为0
	 while (number == 0) {
          //如果余额为0,则该线程进行等待
          this.wait();
      }
      //否则余额减1
      number--;
       //打印存钱信息
       System.out.println(Thread.currentThread().getName()+"取出"+number+"000元");
      //通知其他线程,已经取完钱了
       this.notifyAll();
   }
}
  • 测试结果:

在这里插入图片描述

结果余额符合预期结果,没有出现负数了!

3-3 为什么不能使用if而要使用while?

由于我们对使用wait方法后进入休眠的线程被唤醒时的执行位置有误解,就会认为这两种方式的执行顺序似乎相同;

以saveMoney方法为例

如果使用if进行条件判断 (即判断余额是否等于0), 若条件不满足 (即等于0),当前线程调用wait()方法进行等待,若当前线程被其他线程唤醒,不会重新进行条件判断,而是直接执行number++(余额加一操作);

如果使用while循环进行条件判断,虽然还是直接执行number++ (余额加一操作),但是当前线程被唤醒后,会重新进行条件判断,如果条件不成立(即剩余数量等于0),就会继续进入等待

总结使用if判断线程唤醒后不会重新进行条件判断,而使用while判断线程唤醒后会重新进行条件判断

2.使用Lock锁解决生产者消费者问题

使用Lock锁解决生产者消费者问题,需要调用newCondition()方法,那么什么是newCondition呢?

2.1 newCondition方法和Condition接口
1-1 newCondition方法是什么?

JDK8 的API文档中,我们可以看到如下解释

newCondition()方法是指返回一个新的Condition实例绑定到当前Lock对象; 在等待条件之前必须由当前线程保持,呼叫Condition实例await()方法后,将在等待之前原子锁释放,并且在等待返回之前重新获取到

文档中还提到注意事项Condition实例确切操作取决于Lock锁实现,必须由该实现记录 (意思应该是Lock锁会记录Condition实例的状态)

在这里插入图片描述

提到了newCondition()方法,这里反复出现了一个Condition实例,那么这个Condition实例究竟是指什么呢

在查看了API文档后,我们发现,这个Condition实例本质上是一个接口

它有两个 ConditionObject 实现类,其中一个来自AbstractQueuedLongSynchronizer,另一个来自AbstractQueuedLongSynchronizer

在这里插入图片描述

1-2 什么是condition接口?

Condition接口包含了Object对象的一些监视器方法 (包括wait、notify和notifyAll方法等) ,以得到具有多个等待集的对象,通过将它们进行任意的组合实现Lock锁的效果;使用Lock锁替换 synchronized方法和语句,使用Condition条件来取代对象监视器方法

注意由于这个JDK文档的翻译有些问题,所以有些不通的地方是为根据自己的理解进行修改的

文档中最后还特别补充

一个Condition实例本质上绑定到一个锁,要获得特定Condition实例,请使用其newCondition()方法

在这里插入图片描述

2.2 Condition接口的具体使用
2-1 Condition实例使用官方案例

API文档中给出的Condition使用案例

在这里插入图片描述

从该使用案例中我们可以得知

要想使用Condition实例 (条件),我们首先需要获取一个ReentrantLock对象(重入锁),然后通过调用重入锁的newCondition()方法来获取Condition实例

2-4 使用Lock实现生产者消费者

看完了官方给出的案例,我们把前面使用传统的synchronized同步锁实现的生产者消费者问题使用Lock锁来再实现一下,进一步体会两者的区别的各自的优势

  • 代码实现:
package com.kuang.lock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description 生产者消费者问题(使用Lock锁实现)
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/24
 */


public class ProducerConsumerTest2 {

    public static void main(String[] args) {
        //获取Money资源类
        Money2 money = new Money2();
        //四个线程:A和C存钱,B和D取钱
        //A线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //存钱
                    money.saveMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小A").start();
        //B线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //取钱
                    money.drawMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小B").start();
        //C线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //存钱
                    money.saveMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小C").start();
        //D线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    //取钱
                    money.drawMoney();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"小D").start();
    }

}

//资源类:Money2
class Money2 {

    //钱的余额
    private int number = 0;

    //获取一个重入锁对象实例lock
    Lock lock= new ReentrantLock();
    //获取Condition实例(通过调用lock锁的newCondition()方法获得)
    Condition condition = lock.newCondition();

    //存钱
    public void saveMoney() throws InterruptedException {
        //首先上锁
        lock.lock();
        //执行业务代码
        try {
            //判断余额是否不为0
            while (number != 0) {
                //如果条件满足(即余额为0),则该线程进入等待
                condition.await();
            }
            //否则余额加1
            number++;
            //打印存钱信息
            System.out.println(Thread.currentThread().getName()+"存入后,剩余"+number+"000元");
            //通知所有其他线程(已经存好钱了)
            condition.signalAll();
        //捕获异常
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
            //最后解锁
            lock.unlock();
        }
    }

    //取钱
    public void drawMoney() throws InterruptedException {
        //首先上锁
        lock.lock();
        //执行业务代码
        try {
            //判断余额是否为0
            while (number == 0) {
                //如果条件满足(即余额为0),则该线程进行等待
                condition.await();
            }
            //否则余额减1
            number--;
            //打印存钱信息
            System.out.println(Thread.currentThread().getName()+"取出后,剩余"+number+"000元");
            //通知所有其他线程(已经取完钱了)
            condition.signalAll();
        //捕获异常
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //最后解锁
            lock.unlock();
        }
    }
}
  • 测试结果:
    在这里插入图片描述

结果执行结果符合预期要求,没有出现余额为负数的情况!

2.3 结果分析和使用总结
3-1 结果分析

我们发现,虽然执行的结果符合预期要求,但是其执行结果并不是ABCD四个线程顺序执行,而是随机执行

3-2 使用总结
  • synchronized同步锁中有wait()方法 (等待) 和 notify()方法 (唤醒) ,而在Lock锁中有await()方法 (等待) 和 signal()方法 (唤醒) 方法,有异曲同工之妙!
  • 因此任何一个新的技术,绝对不是覆盖了原来的技术,只是在其基础上做了改进和补充!

3. Condition实现精准通知唤醒

3.1 为什么要实现精准通知唤醒?

在前面,我们分别使用synchronized同步锁和ReentrantLock重入锁,实现了生产者消费者问题,但是我们发现其线程执行过程虽然没有出错,但是线程却是随机执行的;

如果我们能够控制线程顺序执行,就可以解决一些特殊的需求,例如在电商平台交易过 程:加入购物车->下单->支付->收货,接下来就让我们来使用线程模拟一下吧

3.2 如何实现精准唤醒?

我们可以使用condition来实现精准唤醒,例如在加入购物车的业务执行完后,步骤值加1,去唤醒步骤2,执行下单业务,以此类推,便可以实现我们的对顺序执行的需求!

3.3 Condition实现精准唤醒
3-1 代码实现
package com.kuang.lock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


/**
 * @ClassName ProducerConsumerTest3
 * @Description 使用Condition实现精准唤醒
 * 模拟场景:电商平台的交易过程:加入购物车->下单->支付->收货
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/27
 */
public class ProducerConsumerTest4 {
    public static void main(String[] args) {
        //获取资源对象实例trade
        Trade trade = new Trade();
        //模拟A B C D四个线程精准唤醒
        //A线程
        new Thread(()->{
            //使用for循环模拟多次执行
            for (int i = 0; i < 10; i++) {
                //加入购物车
                trade.addCard();
            }
        },"加入购物车").start();
        //B线程
        new Thread(()->{
            //使用for循环模拟多次执行
            for (int i = 0; i < 10; i++) {
                //下单
                trade.order();
            }
        },"下单").start();
        //C线程
        new Thread(()->{
            //使用for循环模拟多次执行
            for (int i = 0; i < 10; i++) {
                //支付
                trade.payment();
            }
        },"支付").start();
        //D线程
        new Thread(()->{
            //使用for循环模拟多次执行
            for (int i = 0; i < 10; i++) {
                //收货
                trade.receive();
            }
        },"收货").start();
    }
}

//资源类:Trade
class Trade {

    //设置初始步骤为1
    private Integer step = 1;

    //获取ReentrantLock(重入锁)对象的实例lock
    private Lock lock = new ReentrantLock();
    //获取Condition对象实例(通过lock锁的newCondition()方法)
    //获取多个condition实例,相当于创建了一个等待队列(执行顺序为1A,2B,3C,4D)
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private Condition condition4 = lock.newCondition();

    //步骤一:加入购物车
    public void addCard() {
        //首先上锁
        lock.lock();
        //执行业务代码(判断->执行->通知)
        try {
            //判断是否为步骤1
            while(step != 1) {
                //如果满足条件(即不为步骤1),则(线程A)进入等待
                condition1.await();
            }
            //如果不满足条件(即是步骤1),则步骤值设置为2
            step=2;
            //打印输出结果
            System.out.println(Thread.currentThread().getName()+"成功!");
            //唤醒指定线程B(下单)
            condition2.signal();
            //捕获异常
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //最后锁定
            lock.unlock();
        }
    }

    //步骤二:下单
    public void order() {
        //首先上锁
        lock.lock();
        //执行业务代码(判断->执行->通知)
        try {
            //判断数量是否不为2
            while(step != 2) {
                //如果满足条件(即为不步骤2),则(线程B)进入等待
                condition2.await();
            }
            //如果不满足条件(即为步骤2),则步骤值加1
            step=3;
            //打印输出结果
            System.out.println(Thread.currentThread().getName()+"成功!");
            //唤醒指定线程C(支付)
            condition3.signal();
            //捕获异常
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //最后锁定
            lock.unlock();
        }
    }

    //步骤三:支付
    public void payment() {
        //首先上锁
        lock.lock();
        //执行业务代码(判断->执行->通知)
        try {
            //判断是否为步骤3
            while(step != 3) {
                //如果满足条件(即步骤不为3),则(线程C)进入等待
                condition3.await();
            }
            //如果不满足条件(即步骤为3),则步骤值加1
            step=4;
            //打印输出结果
            System.out.println(Thread.currentThread().getName()+"成功!");
            //唤醒指定线程D(收货)
            condition4.signal();
            //捕获异常
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //最后锁定
            lock.unlock();
        }
    }

    //步骤四:收货
    public void receive() {
        //首先上锁
        lock.lock();
        //执行业务代码(判断->执行->通知)
        try {
            //判断是否为步骤4
            while(step != 4) {
                //如果满足条件(即步骤不为4),则(线程C)进入等待
                condition4.await();
            }
            //如果不满足条件(即步骤为4),则数量设置为3
            step=1;
            //打印输出结果
            System.out.println(Thread.currentThread().getName()+"成功!");
            //唤醒指定线程A(加入购物车)
            condition1.signal();
            //捕获异常
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //最后锁定
            lock.unlock();
        }
    }

}
3-2 测试结果

在这里插入图片描述

结果按照我们预期的步骤顺序执行,实现了精准唤醒!

到这里,今天的有关生产者消费者问题的学习就结束了,欢迎大家学习和讨论!

参考视频链接:https://www.bilibili.com/video/BV1B7411L7tE (B站UP主遇见狂神说的JUC并发编程基础)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狂奔の蜗牛rz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值