Java 入门指南:Java 并发编程模式 —— 生产者-消费者模式

news2024/11/19 15:17:53

文章目录

    • 生产者-消费者问题
      • 解决方案
    • 生产者-消费者模式
      • 模式的核心问题
      • 基本原理
        • 生产者
        • 消费者
      • 优点
      • 实现方式
        • 使用阻塞队列
          • 示例代码
        • 使用 `wait/notify` 机制
          • wait()
          • notify()
          • notifyAll()
          • 示例代码
        • 使用 `Exchanger`
          • 示例代码
      • 应用场景
      • 总结

生产者-消费者问题

生产者消费者问题是一个经典的并发编程问题,它涉及到多个线程之间共享资源的同步和互斥访问。

在生产者消费者问题中,有两类线程:生产者和消费者。生产者线程负责生产产品并将其放入一个共享的缓冲区,而消费者线程从缓冲区中取出产品并进行消费。但是,缓冲区有限,当缓冲区满时,生产者需要等待;当缓冲区空时,消费者需要等待。

解决方案

为了解决生产者消费者问题中的竞态条件和死锁等并发问题,常见的解决方案有以下几种:

  1. 使用互斥锁和条件变量:生产者和消费者共享一个互斥锁和两个条件变量,通过锁保护共享资源的访问,生产者线程在缓冲区满时等待,消费者线程在缓冲区为空时等待,从而实现线程之间的同步。

  2. 使用信号量:使用两个信号量来表示缓冲区的空和满状态,生产者在缓冲区满时等待,消费者在缓冲区为空时等待,通过对信号量的 P(原语)和 V(原语)操作来实现同步和互斥。

  3. 使用阻塞队列:可以使用具备线程安全的阻塞队列作为缓冲区,这样生产者可以直接将产品放入队列,消费者可以直接从队列中取出产品,队列会自动处理线程之间的同步和互斥。

生产者-消费者模式

生产者消费者模式是一种常见的多线程设计模式,用于解决生产者和消费者之间的解耦和同步问题。

在该模式中,生产者负责生产数据并将其放入共享的缓冲区,而消费者则负责从缓冲区中取出数据进行消费。通过合理地组织生产者和消费者线程的执行顺序和同步操作,可以有效地平衡生产和消费的速度,避免数据竞争和阻塞问题。

模式的核心问题

生产者-消费者模式的关键在于生产者和消费者之间的协调和同步,以确保以下几点:

  1. 生产者在缓冲区满时需要等待:以避免向缓冲区添加数据导致溢出。
  2. 消费者在缓冲区空时需要等待:以避免尝试从空缓冲区中取出数据。
  3. 生产者向缓冲区添加数据后需要唤醒等待中的消费者线程
  4. 消费者从缓冲区取出数据后需要唤醒等待中的生产者线程

基本原理

生产者/消费者模式的核心在于使用一个共享的队列来存储数据,这个队列可以是阻塞队列(BlockingQueue)或者是非阻塞队列(如 LinkedList)。队列的作用是在生产者和消费者之间传递数据,从而实现线程间的解耦。

生产者

生产者线程负责生成数据并将数据放入队列中。生产者的任务通常是数据的采集、计算或者是任何生成数据的操作。生产者在将数据放入队列时,必须确保队列不会溢出。如果队列已满,生产者可能需要等待,直到队列中有空闲的空间。

消费者

消费者线程负责从队列中取出数据并对其进行处理。消费者的任务通常是数据的消费、处理或者是任何使用数据的操作。消费者在从队列中取出数据时,必须确保队列不是空的。如果队列为空,消费者可能需要等待,直到队列中有新的数据可用。

优点

  • 解耦:生产者和消费者之间通过共享队列通信,而不是直接通信,这样就实现了生产者和消费者之间的解耦。生产者不知道也不关心数据会被哪个消费者消费,同样,消费者也不知道数据是由哪个生产者产生的。

  • 通过使用队列,生产者/消费者模式可以平滑负载,即使在短时间内有大量的数据需要处理,队列也可以暂时存储这些数据,防止生产者因为无法立即处理数据而导致的问题。

  • 生产者/消费者模式允许多个生产者和消费者同时工作,提高了系统的并发性。此外,由于队列的存在,生产者和消费者的数量可以灵活调整,以适应不同的工作负载。

实现方式

使用阻塞队列

Java 提供了 BlockingQueue 接口以及其实现类(如 ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue),可以直接用于实现生产者/消费者模式。使用阻塞队列的好处是可以简化线程间的同步逻辑,因为 BlockingQueue 本身提供了线程安全的阻塞方法(如 put()take())。

示例代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ProducerConsumerUsingBlockingQueue {
    private static final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

    public static void main(String[] args) {
        Thread producerThread = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    queue.put(i);
                    System.out.println("Produced: " + i);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        Thread consumerThread = new Thread(() -> {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("Consumed: " + value);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}
使用 wait/notify 机制

如果不使用阻塞队列,可以通过手动实现 wait/notify 机制来控制生产者和消费者之间的同步。这种方式更加灵活,但同时也增加了实现的复杂度。

Java 入门指南:Java 并发编程 —— Condition 灵活管理线程间的同步
在这里插入图片描述

wait()

该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。

在调用 wait 之前,线程必须获得该对象的监视器锁,即只能在同步方法或同步块中调用 wait 方法。调用 wait 方法之后,当前线程会释放锁。如果调用 wait 方法时,线程并未获取到锁的话,则会抛出 IllegalMonitorStateException 异常。如果再次获取到锁的话,当前线程才能从 wait 方法处成功返回。

notify()

该方法也需要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁,如果调用 notify 时没有持有适当的锁,也会抛出 IllegalMonitorStateException

该方法会从 WAITTING 状态的线程中挑选一个进行通知,使得调用 wait 方法的线程从等待队列移入到同步队列中,等待机会再一次获取到锁,从而使得调用 wait 方法的线程能够从 wait 方法处退出。

调用 notify 后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁。

notifyAll()

该方法与 notify 方法的工作方式相同,重要的一点差异是:notifyAll 会使所有原来在该对象上 wait 线程统统退出 WAITTING 状态,使得他们全部从等待队列中移入到同步队列中去,等待下一次获取到对象监视器锁的机会。

示例代码
import java.util.LinkedList;

public class ProducerConsumerUsingWaitNotify {
    private static final LinkedList<Integer> queue = new LinkedList<>();
    private static final int MAX_SIZE = 10;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread producerThread = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                synchronized (lock) {
                    while (queue.size() >= MAX_SIZE) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    queue.add(i);
                    System.out.println("Produced: " + i);
                    lock.notifyAll();
                }
            }
        });

        Thread consumerThread = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    while (queue.isEmpty()) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    int value = queue.removeFirst();
                    System.out.println("Consumed: " + value);
                    lock.notifyAll();
                }
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}
使用 Exchanger

对于需要成对交换数据的情况,可以使用 ExchangerExchanger 允许两个线程交换数据,当一个线程调用 exchange() 方法时,它会等待另一个线程也调用 exchange() 方法,然后两个线程可以交换它们的数据。

示例代码
import java.util.concurrent.Exchanger;

public class ProducerConsumerUsingExchanger {
    private static final Exchanger<String> exchanger = new Exchanger<>();

    public static void main(String[] args) {
        Thread producerThread = new Thread(() -> {
            try {
                String value = "Data from Producer";
                String received = exchanger.exchange(value);
                System.out.println("Producer received: " + received);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumerThread = new Thread(() -> {
            try {
                String value = "Data from Consumer";
                String received = exchanger.exchange(value);
                System.out.println("Consumer received: " + received);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}

应用场景

生产者/消费者模式可以应用于多种场景,以下是一些常见的应用场景:

  • 数据流处理系统中,生产者线程负责收集数据,而消费者线程负责处理数据。这种模式非常适合实时数据分析、日志处理等领域。

  • 图形渲染引擎中,生产者线程负责渲染图像帧,而消费者线程负责显示图像帧。这种模式可以提高渲染速度并减少延迟。

  • 消息队列系统:在消息队列系统中,生产者线程负责发布消息,而消费者线程负责接收消息。这种模式广泛应用于分布式系统中的消息传递。

总结

生产者/消费者模式是一种重要的多线程设计模式,它通过引入共享队列来实现生产者和消费者之间的解耦。这种模式不仅可以提高系统的并发性和灵活性,还可以平滑负载,适用于多种应用场景。通过使用阻塞队列、wait/notify 机制或者 Exchanger,可以方便地实现生产者/消费者模式,并解决多线程环境下数据生产和消费的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2132632.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java项目: 基于SpringBoot+mybatis+maven旅游管理系统(含源码+数据库+毕业论文)

一、项目简介 本项目是一套基于SpringBootmybatismaven旅游管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、…

【SqlServer】SQL Server Management Studio (SSMS) 下载、安装、配置使用及卸载——保姆级教程

超详细的 SQL Server Management Studio (SSMS) 下载、安装、连接数据库配置及卸载教程 SQL Server Management Studio (SSMS) 是微软提供的图形化管理工具&#xff0c;主要用于连接、管理和开发 SQL Server 数据库。以下是详细的 SSMS 下载、安装、连接数据库以及卸载的完整教…

CLIP:Learning Transferable Visual Models From Natural Language Supervision

论文:https://arxiv.org/abs/2103.00020 代码:https://github.com/openai/CLIP 官博:https://openai.com/index/clip/ 复现:https://github.com/mlfoundations/open_clip 基础知识 InfoNCE loss

S7-1500T分布式同步功能

1. 功能描述工控人加入PLC工业自动化精英社群 在一些实际应用中&#xff0c;会需要很多轴进行同步运行&#xff0c;如印刷机、纸尿裤生产线等。由于一个 PLC 的运动控制资源有限&#xff0c;控制轴的数量也是有限的&#xff0c;就会需要多个 PLC 间协调实现轴工艺对象的跨CPU的…

使用Cerbot---Let’s Encrypt生成免费的ssl证书,并设置自动更新证书

安装Certbot客户端 yum install certbot 获取证书 certbot certonly --webroot -w /var/www/demo.com -d demo.com 按照步骤 输入邮箱 同意条例 成功申请证书 修改对应的nginx的conf文件 server {listen 80;listen [::]:80;server_name demo.com;# 将 HTTP 请求重定向到 H…

分布式事务学习笔记(一)分布式事务问题、CAP定理、BASE理论、Seata

文章目录 1 分布式事务问题1.1 本地事务1.2 分布式事务1.3 创建分布式事务演示案例 2 理论基础2.1 CAP定理2.2 BASE理论2.3 解决分布式事务的思路2.4 Seata 1 分布式事务问题 1.1 本地事务 本地事务&#xff0c;也就是传统的单机事务&#xff0c;它必须要满足以下四个原则&am…

RabbitMQ延迟消息——DelayExchange插件

什么是死信以及死信交换机 当一个队列中的消息满足下列情况之一时&#xff0c;可以成为死信&#xff1a; 1. 消费者使用basic.reject或 basic.nack声明消费失败&#xff0c;并且消息的requeue参数设置为false 2. 消息是一个过期消息&#xff0c;超时无人消费 3. 要投递的队列消…

【JavaSE】--方法的使用

文章目录 1. 方法概念及使用1.1 什么是方法1.2 方法定义1.3 方法调用的执行过程1.4 实参和形参的关系&#xff08;重要&#xff09;1.5 没有返回值的方法 2. 方法重载2.1 方法重载概念2.2 方法签名 3. 递归3.1 递归的概念3.2 递归执行过程分析3.3 递归练习 1. 方法概念及使用 1…

解码3D数字人及AIGC产品,如何赋能医美行业全场景业务增长

9月13日&#xff0c;第六届“医美小小聚”暨医美信息与服务创新发展大会在热烈的氛围中拉开帷幕。此次盛会汇聚了医美行业的顶尖精英与前瞻者&#xff0c;他们围绕“聚焦营销&#xff0c;合规增长&#xff0c;融合共创”的主题&#xff0c;深入剖析了行业的新趋势、新机遇与新挑…

SpringBoot开发——整合SSL证书启用HTTPS协议

文章目录 1、https协议2、SpringBoot项目启用HTTPS协议过程2.1 创建SpringBoot项目2.2 准备SSL证书2.3SpringBoot设置2.4启动项目 1、https协议 网站使用的协议包括&#xff1a;http协议和https协议。http协议就是网址以http://开头的&#xff0c;https协议就是网址以https://…

http连接github远程仓库密码问题解决办法

目录 一、问题&#xff1a;使用http连接失败 二、解决办法&#xff1a;使用个人访问令牌。 1、生成访问令牌&#xff1a; 步骤 1: 登录 GitHub 步骤 2: 进入设置页面 步骤 3: 生成新的访问令牌 步骤 4: 配置访问令牌 步骤 5: 复制令牌 2. 使用访问令牌 一、问题&#…

从卷积的物理意义出发的第二种卷积计算方法

禹晶、肖创柏、廖庆敏《数字图像处理&#xff08;电子信息前沿技术丛书&#xff09;》P78 第一&#xff0c;从物理意义理解卷积&#xff0c;为什么要卷&#xff0c;为什么要积。全名卷积和&#xff0c;先卷&#xff0c;再积&#xff0c;后和。 第二&#xff0c;这种方式计算节…

类加载机制和双亲委派

打印一个类加载器的示例。 import java.net.URL; import sun.misc.Launcher;public class TestJDKClassLoader {public static void main(String[] args) {System.out.println(String.class.getClassLoader());System.out.println(com.sun.crypto.provider.DESKeyFactory.clas…

Mysql 搭建主从复制

Docker Mysql 镜像启动命令(主库) docker run --name mysql-master -ti -d --privileged"true" -p 3306:3306 alibaba-cloud-linux-3-registry.cn-hangzhou.cr.aliyuncs.com/alinux3/mysql_optimized:20240221-8.0.32-2.3.0 mysql_keentune.sh 修改临时密码 如果您…

OpenCV 4.10 windows 上编译并上传conan

目录 一. 上传opencv 预编译包 二. 自己手动写一个测试包并上传 三. 自己写一个app, 引用包 一. 上传opencv 预编译包 1. 下载Opencv, 并用cmake 打开 打开工程之后&#xff0c;编译&#xff0c;install&#xff0c; 目录如下 2. 准备conan 包 把Debug 和 Release 分开放 3…

CTFHub技能树-密码口令-弱口令

目录 前提知识 BrupSuite爆破的四种模式详解 解题过程 通常认为容易被别人&#xff08;他们有可能对你很了解&#xff09;猜测到或被破解工具破解的口令均为弱口令。 前提知识 BrupSuite爆破的四种模式详解 四种模式分别为&#xff1a;Sniper、Battering ram、Pitchfork、…

Visual Studio 2022从外部引入dll导致的问题

这里以我学MapGIS二次开发的一个小demo为例 一、如何引入dll 1、在解决方案资源管理器中&#xff0c;有个引用的选项 2、然后右键点击添加引用 点击之后会出现如下&#xff1a; 3、点击浏览选项&#xff0c;选择想要引入dll的路径&#xff0c;这里我选择下载MapGIS 10的路径 …

[LitCTF 2024]SAS - Serializing Authentication

题目提示反序列化 源码 <?phpclass User {public $username;public $password;function __construct($username, $password) {$this->username $username;$this->password $password;}function isValid() { return $this->username admin && $this-&g…

《JavaEE进阶》----17.<Mybatis基本操作【注解XML】>

本篇博客详细讲解了&#xff1a;编写SQL语句 1.使用注释 2.使用XML 3.多表查询 前言&#xff1a; Mybatis规范中方法名不能重复&#xff0c;即便参数不同。因为每一个方法名都是有一个唯一的ID标识的。因此不能重复。 我们会将数据库相关的接口放在Mapper包下面。 对&#xff0…

Latex输入数学期望E及花体符号方法

一、数学期望 E \mathbb{E} E 使用Latex写论文时&#xff0c;我们希望输入如下图所示的数学期望&#xff1a; 方法如下&#xff1a; 首先需要导入\usepackage{amssymb}包&#xff0c;然后正文使用描述为\mathbb{E} 二、花体 B \mathcal{B} B 此外&#xff0c;当我们希望…