并发王者课-青铜10:千锤百炼-如何解决生产者与消费者经典问题

欢迎来到《并发王者课》,本文是该系列文章中的第10篇

在本篇文章中,我将为你介绍并发中的经典问题-生产者与消费者问题,并基于前面系列文章的知识点,通过wait、notify实现这一问题的简版方案

一、生产者与消费者问题

生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多进程、线程同步问题的经典案例。

这个问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。

生产者与消费者问题的关键在于要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据

同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用线程间通信的方法解决该问题,常用的方法有信号量等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。

当然,生产者与消费者问题并不是局限于单个生产者与消费者,在实际工作中,遇到更多的是多个生产者和消费者的情形

生产者与消费者模式在软件开发与设计中有着非常广泛的应用。在这一模式中,生产者与消费者相互独立,它们仅通过缓冲区传递数据,因此可以用于程序间的解耦异步削峰等。

生产者与消费者问题的要点:

  • 生产者与消费者解耦,两者通过缓冲区传递数据
  • 缓冲区数据装满了之后,生产者停止数据生产或丢弃数据
  • 缓冲区数据为空后,消费者停止消费并进入等待状态,等待生产者通知

二、实现生产者与消费者方案

本节中,我们通过王者中的一个场景来模拟生产者与消费者问题。

在王者中,英雄兰陵王需要通过打野来发育,但是野区的野怪在被打完之后,需要隔一段时间再投放。

所以,我们创建两个线程,一个作为生产者向野区投放野怪,一个作为消费者打怪。

生产者:每秒检查一次野区,如果野区没有野怪,则进行投放。野怪投放后,通知打野英雄

// 野怪投放【生产者】
public static class WildMonsterProducer implements Runnable {
  public void run() {
    try {
      createWildMonster();
    } catch (InterruptedException e) {
      System.out.println("野怪投放被中断");
    }
  }

  //投放野怪,每1秒检查一次
  public void createWildMonster() throws InterruptedException {
    for (int i = 0;; i++) {
      synchronized(wildMonsterArea) {
        if (wildMonsterArea.size() == 0) {
          wildMonsterArea.add("野怪" + i);
          System.out.println(wildMonsterArea.getLast());
          wildMonsterArea.notify();
        }
      }
      Thread.sleep(1000);
    }
  }
}

消费者:打野英雄兰陵王作为消费者,在野区打怪发育。如果野区有野怪,则打掉野怪。 如果没有,会进行等待野区新的野怪产生

// 兰陵王,打野英雄
public static class LanLingWang implements Runnable {
  public void run() {
    try {
      attackWildMonster();
    } catch (InterruptedException e) {
      System.out.println("兰陵王打野被中断");
    }
  }

  // 打野,如果没有则进行等待
  public void attackWildMonster() throws InterruptedException {
    while (true) {
      synchronized(wildMonsterArea) {
        if (wildMonsterArea.size() == 0) {
          wildMonsterArea.wait();
        }
        String wildMonster = wildMonsterArea.getLast();
        wildMonsterArea.remove(wildMonster);
        System.out.println("收获野怪:" + wildMonster);
      }
    }
  }
}

创建野区,并启动生产者与消费者线程。

public class ProducerConsumerProblemDemo {

    // 野怪活动的野区
    private static final LinkedList<String> wildMonsterArea = new LinkedList<String>();

    public static void main(String[] args) {
        Thread wildMonsterProducerThread = new Thread(new WildMonsterProducer());
        Thread lanLingWangThread = new Thread(new LanLingWang());

        wildMonsterProducerThread.start();
        lanLingWangThread.start();
    }
}

在上面几段代码中,你需要重点注意的是synchronizedwaitnotify用法,它们是本次方案的关键。运行结果如下:

野怪0
收获野怪:野怪0
野怪1
收获野怪:野怪1
野怪2
收获野怪:野怪2
野怪3
收获野怪:野怪3
野怪4
收获野怪:野怪4
野怪5
收获野怪:野怪5
野怪6
收获野怪:野怪6

从结果可以看到,生产者在创建野怪后,打野英雄兰陵王会进行打野,实现了生产者与消费者的问题。

小结

以上就是关于线程异常处理的全部内容,在本文中我们基于waitnotify来解决生产者与消费者问题。对于本文内容,你需要理解生产者与消费者问题的核心是什么。另外,本文所提供的方案仅仅是这一问题多种解决方案中的一种,在后面的文章中,我们会根据新的知识点提供其他的解法。

正文到此结束,恭喜你又上了一颗星✨

夫子的试炼

  • 编写代码实现生产者与消费者问题。

延伸阅读

关于作者

关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不做标题党。

如果本文对你有帮助,欢迎点赞关注监督,我们一起从青铜到王者

posted @ 2021-06-10 12:23  秦二爷  阅读(26)  评论(0编辑  收藏  举报