当前位置: 首页 > news >正文

生产者-消费者模式:多线程并发协作的经典案例

生产者-消费者模式是多线程并发编程中一个非常经典的模式,它通过解耦生产者和消费者的关系,使得两者可以独立工作,从而提高系统的并发性和可扩展性。本文将详细介绍生产者-消费者模式的概念、实现方式以及应用场景。

1 生产者-消费者模式概述

生产者-消费者模式包含两类线程:

  • 生产者线程:负责生产数据,并将数据放入共享数据区。
  • 消费者线程:负责从共享数据区中取出数据并进行消费。

为了解耦生产者和消费者的关系,通常会使用一个共享的数据区域(如队列)作为缓冲区。生产者将数据放入缓冲区,消费者从缓冲区中取出数据,两者之间不需要直接通信。

共享数据区域需要具备以下功能:

  • 当缓冲区已满时,阻塞生产者线程,防止其继续生产数据。
  • 当缓冲区为空时,阻塞消费者线程,防止其继续消费数据。

2 wait/notify 的消息通知机制

wait/notify 是 Java 中用于线程间通信的经典机制,通过 Object 类提供的 waitnotify/notifyAll 方法,可以实现线程的等待和唤醒。下面将详细介绍 wait/notify 机制的使用方法、注意事项以及常见问题的解决方案。

2.1 wait 方法

wait 方法用于将当前线程置入休眠状态,直到其他线程调用 notifynotifyAll 方法唤醒它。

  • 调用条件wait 方法必须在同步方法或同步块中调用,即线程必须持有对象的监视器锁(monitor lock)。
  • 释放锁:调用 wait 方法后,当前线程会释放锁,并进入等待状态。
  • 异常:如果线程在调用 wait 方法时没有持有锁,则会抛出 IllegalMonitorStateException 异常。

示例代码:

synchronized (lockObject) {try {lockObject.wait();} catch (InterruptedException e) {e.printStackTrace();}
}

2.2 notify 方法

notify 方法用于唤醒一个正在等待该对象监视器锁的线程。

  • 调用条件notify 方法也必须在同步方法或同步块中调用,线程必须持有对象的监视器锁。
  • 唤醒线程notify 方法会从等待队列中随机选择一个线程进行唤醒,使其从 wait 方法处退出,并进入同步队列,等待获取锁。
  • 释放锁:调用 notify 后,当前线程不会立即释放锁,而是在退出同步块后才会释放锁。

示例代码:

synchronized (lockObject) {lockObject.notify();
}

2.3 notifyAll 方法

notifyAll 方法与 notify 方法类似,但它会唤醒所有正在等待该对象监视器锁的线程。

  • 唤醒所有线程notifyAll 方法会将所有等待队列中的线程移入同步队列,等待获取锁。

示例代码:

synchronized (lockObject) {lockObject.notifyAll();
}

2.4 wait/notify 机制的常见问题及解决方案

2.4.1 notify 早期通知

问题描述:如果 notify 方法在 wait 方法之前被调用,可能会导致通知遗漏,使得等待的线程一直处于阻塞状态。

示例代码:

public class EarlyNotify {private static String lockObject = "";public static void main(String[] args) {WaitThread waitThread = new WaitThread(lockObject);NotifyThread notifyThread = new NotifyThread(lockObject);notifyThread.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}waitThread.start();}static class WaitThread extends Thread {private String lock;public WaitThread(String lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {try {System.out.println(Thread.currentThread().getName() + "  进去代码块");System.out.println(Thread.currentThread().getName() + "  开始wait");lock.wait();System.out.println(Thread.currentThread().getName() + "   结束wait");} catch (InterruptedException e) {e.printStackTrace();}}}}static class NotifyThread extends Thread {private String lock;public NotifyThread(String lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {System.out.println(Thread.currentThread().getName() + "  进去代码块");System.out.println(Thread.currentThread().getName() + "  开始notify");lock.notify();System.out.println(Thread.currentThread().getName() + "   结束开始notify");}}}
}

解决方案:添加一个状态标志,在 wait 方法调用前判断状态是否已经改变。

优化后的代码:

public class EarlyNotify {private static String lockObject = "";private static boolean isWait = true;public static void main(String[] args) {WaitThread waitThread = new WaitThread(lockObject);NotifyThread notifyThread = new NotifyThread(lockObject);notifyThread.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}waitThread.start();}static class WaitThread extends Thread {private String lock;public WaitThread(String lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {try {while (isWait) {System.out.println(Thread.currentThread().getName() + "  进去代码块");System.out.println(Thread.currentThread().getName() + "  开始wait");lock.wait();System.out.println(Thread.currentThread().getName() + "   结束wait");}} catch (InterruptedException e) {e.printStackTrace();}}}}static class NotifyThread extends Thread {private String lock;public NotifyThread(String lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {System.out.println(Thread.currentThread().getName() + "  进去代码块");System.out.println(Thread.currentThread().getName() + "  开始notify");lock.notifyAll();isWait = false;System.out.println(Thread.currentThread().getName() + "   结束开始notify");}}}
}

2.4.2 等待条件发生变化

问题描述:如果线程在等待时接收到通知,但之后等待的条件发生了变化,可能会导致程序出错。

示例代码:

public class ConditionChange {private static List<String> lockObject = new ArrayList();public static void main(String[] args) {Consumer consumer1 = new Consumer(lockObject);Consumer consumer2 = new Consumer(lockObject);Productor productor = new Productor(lockObject);consumer1.start();consumer2.start();productor.start();}static class Consumer extends Thread {private List<String> lock;public Consumer(List lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {try {if (lock.isEmpty()) {System.out.println(Thread.currentThread().getName() + " list为空");System.out.println(Thread.currentThread().getName() + " 调用wait方法");lock.wait();System.out.println(Thread.currentThread().getName() + "  wait方法结束");}String element = lock.remove(0);System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element);} catch (InterruptedException e) {e.printStackTrace();}}}}static class Productor extends Thread {private List<String> lock;public Productor(List lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {System.out.println(Thread.currentThread().getName() + " 开始添加元素");lock.add(Thread.currentThread().getName());lock.notifyAll();}}}
}

解决方案:在 wait 方法退出后再次检查等待条件。

优化后的代码:

public class ConditionChange {private static List<String> lockObject = new ArrayList();public static void main(String[] args) {Consumer consumer1 = new Consumer(lockObject);Consumer consumer2 = new Consumer(lockObject);Productor productor = new Productor(lockObject);consumer1.start();consumer2.start();productor.start();}static class Consumer extends Thread {private List<String> lock;public Consumer(List lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {try {while (lock.isEmpty()) {System.out.println(Thread.currentThread().getName() + " list为空");System.out.println(Thread.currentThread().getName() + " 调用wait方法");lock.wait();System.out.println(Thread.currentThread().getName() + "  wait方法结束");}String element = lock.remove(0);System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element);} catch (InterruptedException e) {e.printStackTrace();}}}}static class Productor extends Thread {private List<String> lock;public Productor(List lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {System.out.println(Thread.currentThread().getName() + " 开始添加元素");lock.add(Thread.currentThread().getName());lock.notifyAll();}}}
}

2.4.3 “假死”状态

问题描述:在多消费者和多生产者的情况下,使用 notify 方法可能会导致“假死”状态,即所有线程都处于等待状态,无法被唤醒。

原因分析:如果多个生产者线程调用了 wait 方法阻塞等待,其中一个生产者线程获取到对象锁后使用 notify 方法唤醒其他线程,如果唤醒的仍然是生产者线程,就会导致所有生产者线程都处于等待状态。

解决方案:将 notify 方法替换成 notifyAll 方法。

总结:在使用 wait/notify 机制时,应遵循以下原则:

  • 永远在 while 循环中对条件进行判断,而不是在 if 语句中进行 wait 条件的判断。
  • 使用 notifyAll 而不是 notify

基本的使用范式如下:

synchronized (sharedObject) {while (condition) {sharedObject.wait();// (Releases lock, and reacquires on wakeup)}// do action based upon condition e.g. take or put into queue
}

3 实现生产者-消费者模式的三种方式

生产者-消费者模式可以通过以下三种方式实现:

3.1 使用 Objectwait/notify 机制

Object 类提供了 waitnotify/notifyAll 方法,用于线程间的通信。

  • wait 方法:使当前线程进入等待状态,直到其他线程调用 notifynotifyAll 方法唤醒它。调用 wait 方法前,线程必须持有对象的监视器锁,否则会抛出 IllegalMonitorStateException 异常。
  • notify 方法:唤醒一个正在等待该对象监视器锁的线程。如果有多个线程在等待,则随机选择一个唤醒。
  • notifyAll 方法:唤醒所有正在等待该对象监视器锁的线程。

示例代码:

public class ProductorConsumer {private static LinkedList<Integer> list = new LinkedList<>();private static final int MAX_SIZE = 10;public static void main(String[] args) {ExecutorService service = Executors.newFixedThreadPool(15);for (int i = 0; i < 5; i++) {service.submit(new Productor());}for (int i = 0; i < 10; i++) {service.submit(new Consumer());}}static class Productor implements Runnable {@Overridepublic void run() {while (true) {synchronized (list) {try {while (list.size() == MAX_SIZE) {System.out.println("生产者" + Thread.currentThread().getName() + "  list以达到最大容量,进行wait");list.wait();}int data = new Random().nextInt();System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + data);list.add(data);list.notifyAll();} catch (InterruptedException e) {e.printStackTrace();}}}}}static class Consumer implements Runnable {@Overridepublic void run() {while (true) {synchronized (list) {try {while (list.isEmpty()) {System.out.println("消费者" + Thread.currentThread().getName() + "  list为空,进行wait");list.wait();}int data = list.removeFirst();System.out.println("消费者" + Thread.currentThread().getName() + "  消费数据:" + data);list.notifyAll();} catch (InterruptedException e) {e.printStackTrace();}}}}}
}

3.2 使用 LockConditionawait/signal 机制

LockCondition 提供了比 Objectwait/notify 更灵活的线程通信机制。Condition 对象可以通过 lock.newCondition() 创建,并提供了 awaitsignal/signalAll 方法。

  • await 方法:使当前线程进入等待状态,直到其他线程调用 signalsignalAll 方法唤醒它。
  • signal 方法:唤醒一个正在等待该 Condition 的线程。
  • signalAll 方法:唤醒所有正在等待该 Condition 的线程。

示例代码:

public class ProductorConsumer {private static LinkedList<Integer> list = new LinkedList<>();private static final int MAX_SIZE = 10;private static ReentrantLock lock = new ReentrantLock();private static Condition full = lock.newCondition();private static Condition empty = lock.newCondition();public static void main(String[] args) {ExecutorService service = Executors.newFixedThreadPool(15);for (int i = 0; i < 5; i++) {service.submit(new Productor());}for (int i = 0; i < 10; i++) {service.submit(new Consumer());}}static class Productor implements Runnable {@Overridepublic void run() {while (true) {lock.lock();try {while (list.size() == MAX_SIZE) {System.out.println("生产者" + Thread.currentThread().getName() + "  list以达到最大容量,进行wait");full.await();}int data = new Random().nextInt();System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + data);list.add(data);empty.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}}static class Consumer implements Runnable {@Overridepublic void run() {while (true) {lock.lock();try {while (list.isEmpty()) {System.out.println("消费者" + Thread.currentThread().getName() + "  list为空,进行wait");empty.await();}int data = list.removeFirst();System.out.println("消费者" + Thread.currentThread().getName() + "  消费数据:" + data);full.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}}
}

3.3 使用 BlockingQueue 实现

BlockingQueue 是 Java 并发包中提供的一个接口,它提供了可阻塞的插入和移除操作。BlockingQueue 非常适合用来实现生产者-消费者模型,因为它可以自动处理线程的阻塞和唤醒。

  • put 方法:如果队列已满,则阻塞生产者线程,直到队列有空闲空间。
  • take 方法:如果队列为空,则阻塞消费者线程,直到队列中有数据。

示例代码:

public class ProductorConsumer {private static LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);public static void main(String[] args) {ExecutorService service = Executors.newFixedThreadPool(15);for (int i = 0; i < 5; i++) {service.submit(new Productor());}for (int i = 0; i < 10; i++) {service.submit(new Consumer());}}static class Productor implements Runnable {@Overridepublic void run() {try {while (true) {int data = new Random().nextInt();System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + data);queue.put(data);}} catch (InterruptedException e) {e.printStackTrace();}}}static class Consumer implements Runnable {@Overridepublic void run() {try {while (true) {int data = queue.take();System.out.println("消费者" + Thread.currentThread().getName() + "  消费数据:" + data);}} catch (InterruptedException e) {e.printStackTrace();}}}
}

4 生产者-消费者模式的应用场景

生产者-消费者模式广泛应用于以下场景:

  • 任务执行框架:如 Executor 框架,将任务的提交和执行解耦,提交任务的操作相当于生产者,执行任务的操作相当于消费者。
  • 消息中间件:如 MQ(消息队列),用户下单相当于生产者,处理订单的线程相当于消费者。
  • 任务处理时间较长:如上传附件并处理,用户上传附件相当于生产者,处理附件的线程相当于消费者。

5 生产者-消费者模式的优点

  • 解耦:生产者和消费者之间通过缓冲区进行通信,彼此独立,简化了系统的复杂性。
  • 复用:生产者和消费者可以独立复用和扩展,提高了代码的可维护性。
  • 调整并发数:可以根据生产者和消费者的处理速度调整并发数,优化系统性能。
  • 异步:生产者和消费者各司其职,生产者不需要等待消费者处理完数据,消费者也不需要等待生产者生产数据,提高了系统的响应速度。
  • 支持分布式:生产者和消费者可以通过分布式队列进行通信,支持分布式系统的扩展。

6 小结

生产者-消费者模式是多线程并发编程中的经典模式,通过解耦生产者和消费者的关系,提高了系统的并发性和可扩展性。本文介绍了三种实现生产者-消费者模式的方式,并给出了相应的示例代码。通过理解这些实现方式,可以更好地应用生产者-消费者模式解决实际问题。

7 思维导图

在这里插入图片描述

8 参考链接

从根上理解生产者-消费者模式

相关文章:

生产者-消费者模式:多线程并发协作的经典案例

生产者-消费者模式是多线程并发编程中一个非常经典的模式&#xff0c;它通过解耦生产者和消费者的关系&#xff0c;使得两者可以独立工作&#xff0c;从而提高系统的并发性和可扩展性。本文将详细介绍生产者-消费者模式的概念、实现方式以及应用场景。 1 生产者-消费者模式概述…...

数据库-mysql(基本语句)

演示工具&#xff1a;navicat 连接&#xff1a;mydb 一.操作数据库 1.创建数据库 ①create database 数据库名称 //普通创建 ②create database if not exists 数据库名称 //创建数据库&#xff0c;判断不存在&#xff0c;再创建&#xff1a; 使用指定数据库 use 数据库…...

android12L super.img 解压缩及其挂载到ubuntu18.04

本文介绍如何在Ubuntu18.04上解压缩高通平台Android12L的super.img&#xff0c;并将其挂载到系统中查看内容。 在源码的根目录下&#xff0c;执行如下命令&#xff1a; out/host/linux-x86/bin/simg2img out/target/product/msmnile_gvmq/super.img super.img_rawmkdir super…...

flask简易版的后端服务创建接口(python)

1.pip install安装Flask和CORS 2.创建http_server.py文件,内容如下 """ ============================ 简易版的后端服务 ============================ """ from flask import Flask, request, jsonify from flask_cors import CORS app = F…...

小程序入门学习(四)之全局配置

一、 全局配置文件及常用的配置项 小程序根目录下的 app.json 文件是小程序的全局配置文件。常用的配置项如下&#xff1a; pages&#xff1a;记录当前小程序所有页面的存放路径 window&#xff1a;全局设置小程序窗口的外观 tabBar&#xff1a;设置小程序底部的 tabBar 效…...

PHP使用RabbitMQ(正常连接与开启SSL验证后的连接)

代码中包含了PHP在一般情况下使用方法和RabbitMQ开启了SSL验证后的使用方法&#xff08;我这边消费队列是使用接口请求的方式&#xff0c;每次只从中取出一条&#xff09; 安装amqp扩展 PHP使用RabbitMQ前&#xff0c;需要安装amqp扩展&#xff0c;之前文章中介绍了Windows环…...

轻量级视觉骨干网络 MobileMamba: Lightweight Multi-Receptive Visual Mamba Network

MobileMamba 快速链接解决问题&#xff1a;视觉模型在移动设备端性能和效果的平衡性解决方法&#xff1a;改进网络结构训练和测试策略网络结构改进训练和测试策略 实验支撑&#xff1a;图像分类、分割&#xff0c;目标检测等图像分类结果对比目标检测和实例分割结果对比语义分割…...

科技云报到:数智化转型风高浪急,天翼云如何助力产业踏浪而行?

科技云报到原创。 捷径消亡&#xff0c;破旧立新&#xff0c;是今年千行百业的共同底色。 穿越产业周期&#xff0c;用数字化的力量重塑企业经营与增长的逻辑&#xff0c;再次成为数字化技术应用的主旋律&#xff0c;也是下一阶段产业投资的重点。 随着数字化转型行至“深水区…...

dockerfile部署前后端(vue+springboot)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言0.环境说明和准备1.前端多环境打包1.1前端多环境设置1.2打包 2.后端项目多环境配置以及打包2.1后端多环境配置2.2项目打包 3.文件上传4.后端镜像制作4.1dockerf…...

c语言的思维导图

之前已经全部学完c语言了&#xff0c;所以为了更好的复习回顾&#xff0c;我做了一份c语言超详细的思维导图&#xff0c;帮助实现一张图就可以复习&#xff0c;避免盲目&#xff0c; 由于平台不支持直接发上图&#xff0c;有想要的小伙伴&#xff0c;可以私信找我要原件...

Android 拍照(有无存储权限两种方案,兼容Q及以上版本)

在某些行业&#xff0c;APP可能被禁止使用存储权限&#xff0c;或公司在写SDK功能&#xff0c;不方便获取权限 所以需要有 无存储权限拍照方案。这里两种方案都列出里。 对于写入权限&#xff0c;在高版本中&#xff0c;已经废弃&#xff0c; 不可用文件写入读取权限&#xf…...

MongoDB在自动化设备上的应用示例

发现MongoDB特别适合自动化检测数据的存储。。。 例如一个晶圆检测项目&#xff0c;定义其数据结构如下 #pragma once #include <vector> #include <QString> #include <QRectF> #include <string> #include <memory>class tpoWafer; class tp…...

draggable插件——实现元素的拖动排序——拖动和不可拖动的两种情况处理

最近在写后台管理系统的时候&#xff0c;遇到一个需求&#xff0c;就是关于拖动排序的功能。 我之前是写过一个关于拖动表格的功能&#xff0c;此功能可以实现表格中的每一行数据上下拖动实现排序的效果。 vue——实现表格的拖拽排序功能——技能提升 但是目前我这边的需求是…...

Redux的使用

到如今redux的已经不是react程序中必须的一部分内容了&#xff0c; 我们应该在本地需要大量更新全局变量时才使用它! redux vs reducer reducer的工作机制&#xff1a; 手动构造action对象传入dispatch函数中 dispatch函数将 action传入reducer当中 reducer结合当前state与a…...

【JAVA】Java高级:多数据源管理与Sharding:数据分片(Sharding)技术的实现与实践

大规模分布式系统&#xff0c;数据存储和管理变得越来越复杂。随着用户数量和数据量的急剧增加&#xff0c;单一数据库往往难以承载如此庞大的负载。这时&#xff0c;数据分片&#xff08;Sharding&#xff09;技术应运而生。数据分片是一种将数据水平切分到多个数据库实例的技…...

ASP.NET Core 9.0 静态资产传递优化 (MapStaticAssets )

一、结论 &#x1f4a2;先看结论吧&#xff0c; MapStaticAssets 在大多数情况下可以替换 UseStaticFiles&#xff0c;它已针对为应用在生成和发布时了解的资产提供服务进行了优化。 如果应用服务来自其他位置&#xff08;如磁盘或嵌入资源&#xff09;的资产&#xff0c;则应…...

LeetCode刷题day18——贪心

LeetCode刷题day18——贪心 135. 分发糖果分析&#xff1a; 406. 根据身高重建队列分析&#xff1a;for (auto& p : people) 昨天写了一道&#xff0c;今天写了一道&#xff0c;都有思路&#xff0c;却不能全整对。昨天和小伙伴聊天&#xff0c;说是因为最近作业多&#xf…...

MATLAB Simulink® - 智能分拣系统

系列文章目录 前言 本示例展示了如何在虚幻引擎 环境中对四种不同形状的标准 PVC 管件实施半结构化智能分拣。本示例使用 Universal Robots UR5e cobot 执行垃圾箱拣选任务&#xff0c;从而成功检测并分类物体。cobot 的末端执行器是一个吸力抓手&#xff0c;它使 cobot 能够拾…...

linuxCNC(五)HAL驱动的指令介绍

HAL驱动的构成 指令举例详解 从终端进入到HAL命令行&#xff0c;执行halrun&#xff0c;即可进入halcmd命令行 # halrun指令描述oadrt加载comoonent&#xff0c;loadrt threads name1 period1创建新线程loadusr halmeter加载万用表UI界面loadusr halscope加载示波器UI界面sho…...

STM32 进阶 定时器3 通用定时器 案例2:测量PWM的频率/周期

需求分析 上一个案例我们输出了PWM波&#xff0c;这个案例我们使用输入捕获功能&#xff0c;来测试PWM波的频率/周期。 把测到的结果通过串口发送到电脑&#xff0c;检查测试的结果。 如何测量 1、输入捕获功能主要是&#xff1a;测量输入通道的上升沿和下降沿 2、让第一个…...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…...

生成xcframework

打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式&#xff0c;可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

VB.net复制Ntag213卡写入UID

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例

一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

基于Docker Compose部署Java微服务项目

一. 创建根项目 根项目&#xff08;父项目&#xff09;主要用于依赖管理 一些需要注意的点&#xff1a; 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件&#xff0c;否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...

GitHub 趋势日报 (2025年06月06日)

&#x1f4ca; 由 TrendForge 系统生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日报中的项目描述已自动翻译为中文 &#x1f4c8; 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

Golang——6、指针和结构体

指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...

关于uniapp展示PDF的解决方案

在 UniApp 的 H5 环境中使用 pdf-vue3 组件可以实现完整的 PDF 预览功能。以下是详细实现步骤和注意事项&#xff1a; 一、安装依赖 安装 pdf-vue3 和 PDF.js 核心库&#xff1a; npm install pdf-vue3 pdfjs-dist二、基本使用示例 <template><view class"con…...

STM32标准库-ADC数模转换器

文章目录 一、ADC1.1简介1. 2逐次逼近型ADC1.3ADC框图1.4ADC基本结构1.4.1 信号 “上车点”&#xff1a;输入模块&#xff08;GPIO、温度、V_REFINT&#xff09;1.4.2 信号 “调度站”&#xff1a;多路开关1.4.3 信号 “加工厂”&#xff1a;ADC 转换器&#xff08;规则组 注入…...