java wait()/notify() 实现生产者消费者模式详解

java wait()/notify() 实现生产者消费者模式

java中的多线程会涉及到线程间通信,常见的线程通信方式,例如共享变量、管道流等,这里我们要实现生产者消费者模式,也需要涉及到线程通信,不过这里我们用到了java中的wait()、notify()方法:

wait():进入临界区的线程在运行到一部分后,发现进行后面的任务所需的资源还没有准备充分,所以调用wait()方法,让线程阻塞,等待资源,同时释放临界区的锁,此时线程的状态也从RUNNABLE状态变为WAITING状态;

notify():准备资源的线程在准备好资源后,调用notify()方法通知需要使用资源的线程,同时释放临界区的锁,将临界区的锁交给使用资源的线程。

wait()、notify()这两个方法,都必须要在临界区中调用,即是在synchronized同步块中调用,不然会抛出IllegalMonitorStateException的异常。

实现源码:

生产者线程类:

package threads;
import java.util.List;
import java.util.UUID;
public class Producer extends Thread{
 private List<String> storage;//生产者仓库
 public Producer(List<String> storage) {
  this.storage = storage;
 }
 public void run(){
  //生产者每隔1s生产1~100消息
  long oldTime = System.currentTimeMillis();
  while(true){
   synchronized(storage){
    if (System.currentTimeMillis() - oldTime >= 1000) {
     oldTime = System.currentTimeMillis();
     int size = (int)(Math.random()*100) + 1;
     for (int i = 0; i < size; i++) {
      String msg = UUID.randomUUID().toString();
      storage.add(msg);
     }
     System.out.println("线程"+this.getName()+"生产消息"+size+"条");
     storage.notify();
    }
   }
  }
 }
}

消费者线程类:

package threads;
import java.util.List;
public class Consumer extends Thread{
 private List<String> storage;//仓库
 public Consumer(List<String> storage) {
  this.storage = storage;
 }
 public void run(){
  while(true){
   synchronized(storage){
    //消费者去仓库拿消息的时候,如果发现仓库数据为空,则等待
    if (storage.isEmpty()) {
     try {
      storage.wait();
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
    }
    int size = storage.size();
    for (int i = size - 1; i >= 0; i--) {
     storage.remove(i);
    }
    System.out.println("线程"+this.getName()+"成功消费"+size+"条消息");
   }
  }
 }
}

仓库类:

package threads;
import java.util.ArrayList;
import java.util.List; 

public class Storage {
 private List<String> storage;//生产者和消费者共享的仓库
 public Storage() {
  storage = new ArrayList<String>();
 }
 public List<String> getStorage() {
  return storage;
 }
 public void setStorage(List<String> storage) {
  this.storage = storage;
 }
}

main方法类:

package threads;
public class App {
 public static void main(String[] args) {
  Storage storage = new Storage();
  Producer producer = new Producer(storage.getStorage());
  Consumer consumer = new Consumer(storage.getStorage());
  producer.start();
  consumer.start();
 }
}

生产消费效果:

Wait/Notify通知机制解析

前言

我们知道,java的wait/notify的通知机制可以用来实现线程间通信。wait表示线程的等待,调用该方法会导致线程阻塞,直至另一线程调用notify或notifyAll方法才可另其继续执行。经典的生产者、消费者模式即是使用wait/notify机制得以完成。在这篇文章中,我们将深入解析这一机制,了解其背后的原理。

线程的状态

在了解wait/notify机制前,先熟悉一下java线程的几个生命周期。分别为初始(NEW)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)、终止(TERMINATED)等状态(位于java.lang.Thread.State枚举类中)。

以下是对这几个状态的简要说明,详细说明见该类注释。

状态名称 说明
NEW 初始状态,线程被构建,但未调用start()方法
RUNNABLE 运行状态,调用start()方法后。在java线程中,将操作系统线程的就绪和运行统称运行状态
BLOCKED 阻塞状态,线程等待进入synchronized代码块或方法中,等待获取锁
WAITING 等待状态,线程可调用wait、join等操作使自己陷入等待状态,并等待其他线程做出特定操作(如notify或中断)
TIMED_WAITING 超时等待,线程调用sleep(timeout)、wait(timeout)等操作进入超时等待状态,超时后自行返回
TERMINATED 终止状态,线程运行结束

对于以上线程间的状态及转化关系,我们需要知道

  • WAITING(等待状态)和TIMED_WAITING(超时等待)都会令线程进入等待状态,不同的是TIMED_WAITING会在超时后自行返回,而WAITING则需要等待至条件改变。
  • 进入阻塞状态的唯一前提是在等待获取同步锁。java注释说的很明白,只有两种情况可以使线程进入阻塞状态:一是等待进入synchronized块或方法,另一个是在调用wait()方法后重新进入synchronized块或方法。下文会有详细解释。
  • Lock类对于锁的实现不会令线程进入阻塞状态,Lock底层调用LockSupport.park()方法,使线程进入的是等待状态。

wait/notify用例

让我们先通过一个示例解析

wait()方法可以使线程进入等待状态,而notify()可以使等待的状态唤醒。这样的同步机制十分适合生产者、消费者模式:消费者消费某个资源,而生产者生产该资源。当该资源缺失时,消费者调用wait()方法进行自我阻塞,等待生产者的生产;生产者生产完毕后调用notify/notifyAll()唤醒消费者进行消费。

以下是代码示例,其中flag标志表示资源的有无。

public class ThreadTest {
    static final Object obj = new Object();
    private static boolean flag = false;
    public static void main(String[] args) throws Exception {
        Thread consume = new Thread(new Consume(), "Consume");
        Thread produce = new Thread(new Produce(), "Produce");
        consume.start();
        Thread.sleep(1000);
        produce.start();
        try {
            produce.join();
            consume.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    // 生产者线程
    static class Produce implements Runnable {
        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("进入生产者线程");
                System.out.println("生产");
                try {
                    TimeUnit.MILLISECONDS.sleep(2000);  //模拟生产过程
                    flag = true;
                    obj.notify();  //通知消费者
                    TimeUnit.MILLISECONDS.sleep(1000);  //模拟其他耗时操作
                    System.out.println("退出生产者线程");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //消费者线程
    static class Consume implements Runnable {
        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("进入消费者线程");
                System.out.println("wait flag 1:" + flag);
                while (!flag) {  //判断条件是否满足,若不满足则等待
                    try {
                        System.out.println("还没生产,进入等待");
                        obj.wait();
                        System.out.println("结束等待");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("wait flag 2:" + flag);
                System.out.println("消费");
                System.out.println("退出消费者线程");
            }
        }
    }
}

输出结果为:

进入消费者线程

wait flag 1:false

还没生产,进入等待

进入生产者线程

生产

退出生产者线程

结束等待

wait flag 2:true

消费

退出消费者线程

理解了输出结果的顺序,也就明白了wait/notify的基本用法。有以下几点需要知道:

  • 在示例中没有体现但很重要的是,wait/notify方法的调用必须处在该对象的锁(Monitor)中,也即,在调用这些方法时首先需要获得该对象的锁。否则会爬出IllegalMonitorStateException异常。
  • 从输出结果来看,在生产者调用notify()后,消费者并没有立即被唤醒,而是等到生产者退出同步块后才唤醒执行。(这点其实也好理解,synchronized同步方法(块)同一时刻只允许一个线程在里面,生产者不退出,消费者也进不去)
  • 注意,消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头。

深入了解

这一节我们探讨wait/notify与线程状态之间的关系。深入了解线程的生命周期。

由前面线程的状态转化图可知,当调用wait()方法后,线程会进入WAITING(等待状态),后续被notify()后,并没有立即被执行,而是进入等待获取锁的阻塞队列。

对于每个对象来说,都有自己的等待队列和阻塞队列。以前面的生产者、消费者为例,我们拿obj对象作为对象锁,配合图示。内部流程如下

  • 当线程A(消费者)调用wait()方法后,线程A让出锁,自己进入等待状态,同时加入锁对象的等待队列。
  • 线程B(生产者)获取锁后,调用notify方法通知锁对象的等待队列,使得线程A从等待队列进入阻塞队列。
  • 线程A进入阻塞队列后,直至线程B释放锁后,线程A竞争得到锁继续从wait()方法后执行。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Java实现Kafka生产者和消费者的示例

    Kafka简介 Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写.Kafka的目标是为处理实时数据提供一个统一.高吞吐.低延迟的平台. 方式一:kafka-clients 引入依赖 在pom.xml文件中,引入kafka-clients依赖: <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId

  • Java如何通过线程解决生产者/消费者问题

    生产者和消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一个存储空间,如下图所示 生产者向空间里存放数据,而消费者取用数据,如果不加以协调可能会出现以下情况: 存储空间已满,而生产者占用着它,消费者等着生产者让出空间从而去除产品,生产者等着消费者消费产品,从而向空间中添加产品.互相等待,从而发生死锁. 以下实例演示了如何通过线程解决生产者/消费者问题: /* author by javaidea.com ProducerConsumerTest.java */ public

  • Java多种方式实现生产者消费者模式

    实现需求:两个线程交替打印1,0,打印10轮 java多线程口诀: 高内聚,低耦合 线程操作资源类 判断干活通知 防止虚假唤醒 方式一:使用synchronized和Object的wait和notifyAll方法 wait:使当前线程阻塞 notify,notifyAll唤醒当前线程 /** * 两个线程交替打印1,0 打印10轮 * * @author Administrator * @version 1.0 2020年7月12日 * @see ProdConsumerDemo1 * @sin

  • Java多线程:生产者与消费者案例

    目录 前言 工具 知识点 设计思路 具体步骤 总结 前言 想象一下生活中哪些是和线程沾边的?饭店炒菜就是一个很好的例子 首先客人要吃菜,前提是厨师要炒好,也就是说,厨师不炒好的话客人是没有饭菜的.这时候,厨师就是一个线程,客人拿菜就是另一个线程. 工具 jdk13,IDEA2019.1.4 知识点 Thread.Runnable.synchronized.面向对象知识(继承.封装.接口.方法重写).条件判断以及线程的一些其他知识点 设计思路 首先要有两个线程,也就是说要两个类,分别是Produc

  • Java实现简易生产者消费者模型过程解析

    一.概述 一共两个线程,一个线程生产产品,一个线程消费产品,使用同步代码块方法,同步两个线程.当产品没有时,通知生产者生产,生产者生产后,通知消费者消费,并等待消费者消费完. 需要注意的是,有可能出现,停止生产产品后,消费者还没未来得及消费生产者生产的最后一个产品,就结束消费,导致最后一个产品没有被消费. 本例使用synchronize以及wait().notify()实现简易版的线程者消费者模型. 二.测试用例 这里的产品用笔来演示,每只笔都有其编号code 一共有四个类:分别是生产者类,产品

  • Java中生产者消费者问题总结

    生产者-消费者算是并发编程中常见的问题.依靠缓冲区我们可以实现生产者与消费者之间的解耦.生产者只管往缓冲区里面放东西,消费者只管往缓冲区里面拿东西.这样我们避免生产者想要交付数据给消费者,但消费者此时还无法接受数据这样的情况发生. wait notify 这个问题其实就是线程间的通讯,所以要注意的是不能同时读写.生产者在缓冲区满的时候不生产,等待:消费者在缓冲区为空的时候不消费,等待.比较经典的做法是wait和notify. 生产者线程执行15次set操作 public class Produc

  • Java实现Kafka生产者消费者代码实例

    Kafka的结构与RabbitMQ类似,消息生产者向Kafka服务器发送消息,Kafka接收消息后,再投递给消费者. 生产者的消费会被发送到Topic中,Topic中保存着各类数据,每一条数据都使用键.值进行保存. 每一个Topic中都包含一个或多个物理分区(Partition),分区维护着消息的内容和索引,它们有可能被保存在不同服务器. 新建一个Maven项目,pom.xml 加入依赖: <dependency> <groupId>org.apache.kafka</gro

  • java wait()/notify() 实现生产者消费者模式详解

    java wait()/notify() 实现生产者消费者模式 java中的多线程会涉及到线程间通信,常见的线程通信方式,例如共享变量.管道流等,这里我们要实现生产者消费者模式,也需要涉及到线程通信,不过这里我们用到了java中的wait().notify()方法: wait():进入临界区的线程在运行到一部分后,发现进行后面的任务所需的资源还没有准备充分,所以调用wait()方法,让线程阻塞,等待资源,同时释放临界区的锁,此时线程的状态也从RUNNABLE状态变为WAITING状态: noti

  • Java多线程之生产者消费者模式详解

    目录 1.生产者消费者模型 2.实现生产者消费者模型 3.生产者消费者模型的作用是什么? 总结 问题: 1.什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型? 2. 生产者消费者模型的作用是什么? 1. 生产者消费者模型 在生产者-消费者模式中,通常有两类线程,即生产者线程(若干个)和消费者线程(若干个).生产者线程向消息队列加入数据,消费者线程则从消息队列消耗数据.生产者和消费者.消息队列之间的关系结构图如图: (1) 消息队列可以用来平衡生产和消费的线程资源: (2) 生产者仅负责产

  • Java多线程 BlockingQueue实现生产者消费者模型详解

    BlockingQueue BlockingQueue.解决了多线程中,如何高效安全"传输"数据的问题.程序员无需关心什么时候阻塞线程,什么时候唤醒线程,该唤醒哪个线程. 方法介绍 BlockingQueue是Queue的子类 void put(E e) 插入指定元素,当BlockingQueue为满,则线程阻塞,进入Waiting状态,直到BlockingQueue有空闲空间再继续. 这里以ArrayBlockingQueue为例进行分析 void take() 队首出队,当Bloc

  • Python之两种模式的生产者消费者模型详解

    第一种使用queue队列实现: #生产者消费者模型 其实服务器集群就是这个模型 # 这里介绍的是非yield方法实现过程 import threading,time import queue q = queue.Queue(maxsize=10) def Producer(anme): # for i in range(10): # q.put('骨头%s'%i) count = 1 while True: q.put('骨头%s'%count) print('生产了骨头',count) cou

  • Java结构型设计模式之组合模式详解

    目录 组合模式 应用场景 优缺点 主要角色 组合模式结构 分类 透明组合模式 创建抽象根节点 创建树枝节点 创建叶子节点 客户端调用 安全组合模式 创建抽象根节点 创建树枝节点 创建叶子节点 客户端调用 组合模式 组合模式(Composite Pattern)也称为整体-部分(Part-Whole)模式,属于结构型模式. 它的宗旨是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口进行表示,使得客户端对单个对象和组合对象的使用具有一致性. 组合模式一般用来描述整体与部分的关系,它将对象

  • Java行为型设计模式之策略模式详解

    目录 1.策略设计模式定义 2.策略设计模式的有点与不足 3.策略设计模式的实现思路 4.代码示例 5.策略设计模式的应用场景 编程是一门艺术,大批量的改动显然是非常丑陋的做法,用心的琢磨写的代码让它变的更美观. 在现实生活中常常遇到实现某种目标存在多种策略可供选择的情况,例如超市促销可以釆用打折.送商品.送积分等方法. 在软件开发中也常常遇到类似的情况,当实现某一个功能存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如数据排序策略有快速排序.归并排序.选

  • Java代码重构的几种模式详解

    Java代码的重构模式主要有三种: 重命名方法重构模式.引入解释性变量重构模式.以查询取代临时变量重构模式 重命名方法重构模式建议执行如下的步骤来完成: 1.建立一个具有新名称的方法 2.将旧方法的方法体复制进新方法 3.讲旧方法的方法体修改为调用新方法 4.将所有引用旧方法的地方修改为引用新方法 5.删除旧方法 引入解释性变量重构模式步骤相对简单,如下: 1.声明一个局部变量,并将其初始化为需要替换的表达式部分 2.对于复杂的表达式,用新的局部变量代替其中需要替换的部分 3.对于该表达式的其他

  • Java多线程之线程通信生产者消费者模式及等待唤醒机制代码详解

    前言 前面的例子都是多个线程在做相同的操作,比如4个线程都对共享数据做tickets–操作.大多情况下,程序中需要不同的线程做不同的事,比如一个线程对共享变量做tickets++操作,另一个线程对共享变量做tickets–操作,这就是大名鼎鼎的生产者和消费者模式. 正文 一,生产者-消费者模式也是多线程 生产者和消费者模式也是多线程的范例.所以其编程需要遵循多线程的规矩. 首先,既然是多线程,就必然要使用同步.上回说到,synchronized关键字在修饰函数的时候,使用的是"this"

  • 基于Java 生产者消费者模式(详细分析)

    生产者消费者模式是多线程中最为常见的模式:生产者线程(一个或多个)生成面包放进篮子里(集合或数组),同时,消费者线程(一个或多个)从篮子里(集合或数组)取出面包消耗.虽然它们任务不同,但处理的资源是相同的,这体现的是一种线程间通信方式. 本文将先说明单生产者单消费者的情况,之后再说明多生产者多消费者模式的情况.还会分别使用wait()/nofity()/nofityAll()机制.lock()/unlock()机制实现这两种模式. 在开始介绍模式之前,先解释下wait().notify()和no

随机推荐