多线程(初阶七:阻塞队列和生产者消费者模型)
目录
一、阻塞队列的简单介绍
二、生产者消费者模型
1、举个栗子:
2、引入生产者消费者模型的意义:
(1)解耦合
(2)削峰填谷
三、模拟实现阻塞队列
1、阻塞队列的简单介绍
2、实现阻塞队列
(1)实现普通队列
(2)加上线程安全
(3)加上阻塞功能
3、运用阻塞队列的生产者消费者模型
都看到这了,点个赞再走吧,谢谢谢谢谢
一、阻塞队列的简单介绍
首先,我们都知道,队列是先进先出的一种数据结构,而阻塞队列,是基于队列,做了一些扩展,在多线程有就非常有意义了
阻塞队列的特性:
(1)是线程安全的
(2)具有阻塞的特性
①当队列满了,这时不能往队列里放数据,就会阻塞等待,等队列的数据出队列后,这时队列没满,才能放数据。
②当队列空了,这时不能拿队列里的数据,就会阻塞等待,等有数据如队列了,这时队列不为空,才能拿数据。
这里,阻塞队列的用处非常大,基于阻塞队列的功能,就可以实现 “生产者消费者模型”。
二、生产者消费者模型
生产者消费者模型是一种很朴素的概念,描述的是一种多线程编程的方法。
1、举个栗子:
一家人包饺子,首先得和面,和完面就要开始擀饺子皮了,然后才开始包饺子,这里,因为一般家里也只有一个擀面杖,所以只能一个人擀饺子皮,其余的家人就会帮忙包饺子,假设一个人擀饺子皮,2个人包饺子,那么擀饺子皮的人就是生产者,包饺子的人就是消费者;这也就是消费者生产者模型。
一个桌子能发饺子皮的数量是有限的,当擀饺子皮的人比较快,桌子放满饺子皮后,就要等包饺子的人,消耗一些饺子皮来包饺子,才能继续擀饺子皮;而这也类似阻塞队列中队里满的情况。当包饺子的人包的比较快,桌子上的饺子皮都没了,就要等擀饺子皮的人擀了饺子皮后才能继续包饺子;而这也类似阻塞队列空的情况。不同的人分工不同,也类似多线程,各自干各自的事情。
2、引入生产者消费者模型的意义:
(1)解耦合
引入生产者消费者模型,就能更好的做到解耦合(把代码的耦合程度,从高降低,就称为解耦合)
在实际开发中,会涉及到 “分布式系统” ,服务器的整个功能不是又一个服务器完成实现的,而是由多个服务器,各自实现各自的一部分功能,再通过网络通信,把这些服务器联系起来,最终完成整个服务器的功能。粗糙流程如图:
而这时,入口服务器与A、B服务器的联系是密切相关的,请求要经过入口服务器,才能传达给A、B服务器,再A、B服务器拿到想要的数据,再返回给入口服务器,通过入口服务器,再把响应传给客户端。如果是这样,那如果请求突然骤升,这时超过入口服务器接收请求的峰值,这时入口服务器就挂了,入口服务器挂了后,A、B服务器拿不到请求,也会挂掉,这就体现了入口服务器和A、B服务器的耦合性比较高。
当我们在入口服务器和A、B服务器之间引入阻塞队列时,如图:
这时,如果入口服务器挂了,但是阻塞队列中还有请求的数据,至少不会因入口服务器挂了,A、B服务器也挂了,这样,入口服务器和A、B服务器的耦合性也就降低了。
(2)削峰填谷
如图,当客户端这边的请求突然骤增时,入口服务器都是比较能抗压的,但是也是有极限的,这时我们引入阻塞队列,就可以把这些请求数据都放进阻塞队列中,形成一个缓冲区,这样,即使外面的请求达到了峰值,也是由阻塞队列来承担,这样就形成了削峰填谷的效果。
注意:这时的阻塞队列,是基于阻塞队列这一数据结构,而实现的服务器程序,所以也叫消息队列
三、模拟实现阻塞队列
1、阻塞队列的简单介绍
在java标准库中,提供了现成的阻塞队列这一数据结构,如图:
是基于队列扩展而来的,队列有的,它也有;我们知道,入队列时可以用offer方法,出队列时可以用poll方法,在阻塞队列中,也有这两个方法,但是这两个方法是不带阻塞功能的;其中,在阻塞队列中,put是在阻塞功能的入队列,take方法是带阻塞功能的出队列。
代码案例:
public class TestDemo1 {public static void main(String[] args) throws InterruptedException {BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);blockingQueue.put("aaa");String s1 = blockingQueue.take();System.out.println("第一个打印:s1 = " + s1);s1 = blockingQueue.take();System.out.println("第二个打印:s1 = " + s1);} }执行结果:
线程卡住不动了,原因是想要第二次出队列时,队列是空的,所以要等队列有元素入队列时,才能出队列,也就是说,这是带有阻塞功能的。
2、实现阻塞队列
阻塞队列是通过循环队列实现的,而队列是依靠数组来实现的,这里的阻塞队列,我们只模拟实现其中的put和take方法。
(1)实现普通队列
代码:
// 为了简单, 不写作泛型的形式. 考虑存储的元素就是单纯的 String class MyBlockingQueue {private String elems[] = null;private int head = 0;//记录头结点private int tail = 0;//记录尾结点private int size = 0;//队列元素个数//构造方法,定义队列的容量大小public MyBlockingQueue(int capacity) {this.elems = new String[capacity];}//入队列public void put(String elem) {//判断容量满了没,满了就不能入队列,要阻塞等待if(size >= this.elems.length) {//阻塞等待,先不写,先实现普通功能的队列return;}//入队列elems[tail] = elem;tail++;//因为是循环队列,所以要判断尾巴有没有超过容量大小下标,超过了就要从0开始了if(tail > elems.length) {tail = 0;}//队列元素要++size++;}//出队列public String take() {String elem = null;//要判断队列是不是空的,空就不能出队列了,要阻塞等待if(size == 0) {//阻塞等待,因为是先实现普通队列的功能,所以后面再补充return null;}elem = elems[head];head++;//因为是循环队列,所以要判断头结点有没有超过容量大小下标,超过了就要0开始了if(head >= elems.length) {head = 0;}//出队列后,队列元素要--size--;return elem;} }每个步骤说明代码中有注释。
测试一下可不可以用,如图:
是可以用的,这样,普通的队列就已经搞好了
(2)加上线程安全
我们想想put和take里面要给哪里上锁,首先,写操作肯定是要加锁的,因为多线程同时执行写操作,肯定是线程不安全的,也就是下面这段代码:
接下来,我们讨论一下这两个代码要不要加锁,以take为例,如图:
我们画一下图,会比较好理解:
如果size = -1,是不符合我们预期的,size最小也只可能是0,不可能是-1,所以我要上面的判断条件也要加锁。
而put也一样,判断条件也要加锁。
代码:
class MyBlockingQueue {Object locker = new Object();private String elems[] = null;private int head = 0;//记录头结点private int tail = 0;//记录尾结点private int size = 0;//队列元素个数//构造方法,定义队列的容量大小public MyBlockingQueue(int capacity) {this.elems = new String[capacity];}//入队列public void put(String elem) {synchronized (locker) {//判断容量满了没,满了就不能入队列,要阻塞等待if(size >= this.elems.length) {//阻塞等待,先不写,先实现普通功能的队列return;}//因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁synchronized (locker) {//入队列elems[tail] = elem;tail++;//因为是循环队列,所以要判断尾巴有没有超过容量大小下标,超过了就要从0开始了if(tail > elems.length) {tail = 0;}//队列元素要++size++;}}}//出队列public String take() {String elem = null;//因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁synchronized (locker) {//要判断队列是不是空的,空就不能出队列了,要阻塞等待if(size == 0) {//阻塞等待,因为是先实现普通队列的功能,所以后面再补充return null;}elem = elems[head];head++;//因为是循环队列,所以要判断头结点有没有超过容量大小下标,超过了就要0开始了if(head >= elems.length) {head = 0;}//出队列后,队列元素要--size--;return elem;}} }
(3)加上阻塞功能
我们要加上阻塞功能,就要在这两条件判断上加上wait,我们用locker的对象给他wait,而且wait必须要在synchronized内使用,这里的locker正好能对应上;当这个队列满时,就阻塞等待,等take方法拿走一个数据时,才给他唤醒。
加上阻塞功能后的代码如下:(不是最终代码,里面还是存在线程安全问题)
class MyBlockingQueue {Object locker = new Object();private String elems[] = null;private int head = 0;//记录头结点private int tail = 0;//记录尾结点private int size = 0;//队列元素个数//构造方法,定义队列的容量大小public MyBlockingQueue(int capacity) {this.elems = new String[capacity];}//入队列public void put(String elem) throws InterruptedException {synchronized (locker) {//判断容量满了没,满了就不能入队列,要阻塞等待if (size >= this.elems.length) {//阻塞等待,先不写,先实现普通功能的队列synchronized (locker) {locker.wait();}}//因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁synchronized (locker) {//入队列elems[tail] = elem;tail++;//因为是循环队列,所以要判断尾巴有没有超过容量大小下标,超过了就要从0开始了if(tail > elems.length) {tail = 0;}//队列元素要++size++;locker.notify();}}}//出队列public String take() throws InterruptedException {String elem = null;//因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁synchronized (locker) {//要判断队列是不是空的,空就不能出队列了,要阻塞等待if (size == 0) {//阻塞等待,因为是先实现普通队列的功能,所以后面再补充synchronized (locker) {locker.wait();}}elem = elems[head];head++;//因为是循环队列,所以要判断头结点有没有超过容量大小下标,超过了就要0开始了if(head >= elems.length) {head = 0;}//出队列后,队列元素要--size--;locker.notify();return elem;}} }加上阻塞功能的判断语句
与其对应的,一定要记住put和take后要notify,不然就会一直阻塞下去,导致程序动不了,如图:
现在进行代码分析:
当put时,队列满了,就要阻塞等待,等take这队列后,就会唤醒put操作,接着put就能入队列了;如果是take就相反,这是符合我们预期的。如果不满也不空时,每次put和take都会notify一次,这时会有影响吗?答案肯定是否定的,不会有影响,因为就算没有其他线程在等待,唤醒也没有事,不会对程序造成啥影响。而且我们现在的代码,一定是要么满,要么空,要么不满也不空。
但是,如果有两个线程同时put,现在队列是满的,A线程先阻塞,B线程也阻塞,这时有第三个线程take一次,把A线程的wait唤醒了,等A执行到下面的notify,A线程里put的notify就会唤醒B线程里的wait,但是因为A线程put了,和第三个线程的take一取一放抵消了,此时队列还是满的,因为A线程里的put把B线程里的wait唤醒了,这时已经是满了的队列还往里放元素,就造成了线程安全问题。
解决方案:把条件判断if换成while循环语句,不是只判断一次,当有其他线程把wait唤醒后,还要再判断一次这个队列是不是满的或者是空的,如果不是满的或者不是空的,才释放这个wait,不然就要继续wait,这样问题也就解决了。
最终代码:
class MyBlockingQueue {Object locker = new Object();private String elems[] = null;private int head = 0;//记录头结点private int tail = 0;//记录尾结点private int size = 0;//队列元素个数//构造方法,定义队列的容量大小public MyBlockingQueue(int capacity) {this.elems = new String[capacity];}//入队列public void put(String elem) throws InterruptedException {synchronized (locker) {//判断容量满了没,满了就不能入队列,要阻塞等待while (size >= this.elems.length) {//阻塞等待,先不写,先实现普通功能的队列synchronized (locker) {locker.wait();}}//因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁synchronized (locker) {//入队列elems[tail] = elem;tail++;//因为是循环队列,所以要判断尾巴有没有超过容量大小下标,超过了就要从0开始了if(tail > elems.length) {tail = 0;}//队列元素要++size++;locker.notify();}}}//出队列public String take() throws InterruptedException {String elem = null;//因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁synchronized (locker) {//要判断队列是不是空的,空就不能出队列了,要阻塞等待while (size == 0) {//阻塞等待,因为是先实现普通队列的功能,所以后面再补充synchronized (locker) {locker.wait();}}elem = elems[head];head++;//因为是循环队列,所以要判断头结点有没有超过容量大小下标,超过了就要0开始了if(head >= elems.length) {head = 0;}//出队列后,队列元素要--size--;locker.notify();return elem;}} }我们看看wait内部,可以看到它内部也是会有说明wait可能会提前被唤醒,就要我们多加一次判断,这样,用while会比用if更好,如图:
在实际开发中,生产者消费者模型,往往是多个生产者,多个消费者;这里的生产者和消费者往往不仅仅是一个线程,也可能是一个独立的服务器,甚至是一组服务器程序。
但生产者消费者模型,最核心的部分还是阻塞队列,可以使用synchronized和wait / notify 达到线程安全与阻塞。
3、运用阻塞队列的生产者消费者模型
简单的生产者消费者模型代码:
public class Test {public static void main(String[] args) {BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);//生产者Thread t1 = new Thread(() -> {int n = 1;while (true) {try {queue.put(n);System.out.println("生产者元素:" + n);n++;Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//消费者Thread t2 = new Thread(() -> {while (true) {try {int n = queue.take();System.out.println("消费者元素:" + n);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();} }执行结果:
可以看到生产者生产一个,消费者就消费一个,继续等待生产者生产元素后再消费。
都看到这了,点个赞再走吧,谢谢谢谢谢
相关文章:
多线程(初阶七:阻塞队列和生产者消费者模型)
目录 一、阻塞队列的简单介绍 二、生产者消费者模型 1、举个栗子: 2、引入生产者消费者模型的意义: (1)解耦合 (2)削峰填谷 三、模拟实现阻塞队列 1、阻塞队列的简单介绍 2、实现阻塞队列 &#…...
区间价值 --- 题解--动态规划
目录 区间价值 题目描述 输入描述: 输出描述: 输入 输出 备注: 思路: 代码: 区间价值 J-区间价值_牛客竞赛动态规划专题班习题课 (nowcoder.com) 时间限制:C/C 2秒,其他语言4秒 空间限制:C/C 262144K&…...
计算机毕业设计 基于大数据的心脏病患者数据分析管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解
博主介绍:✌从事软件开发10年之余,专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ 🍅文末获取源码联系🍅 👇🏻 精…...
20:kotlin 类和对象 --泛型(Generics)
类可以有类型参数 class Box<T>(t: T) {var value t }要创建类实例,需提供类型参数 val box: Box<Int> Box<Int>(1)如果类型可以被推断出来,可以省略 val box Box(1)通配符 在JAVA泛型中有通配符?、? extends E、? super E&…...
我对迁移学习的一点理解(系列2)
文章目录 我对迁移学习的一点理解 我对迁移学习的一点理解 源域和目标域是相对的概念,指的是在迁移学习任务中涉及到的两个不同的数据集或领域。 源域(Source Domain)通常指的是已经进行过训练和学习的数据集,它被用来提取特征、…...
Spring MVC学习随笔-控制器(Controller)开发详解:控制器跳转与作用域(二)视图模板、静态资源访问
学习视频:孙哥说SpringMVC:结合Thymeleaf,重塑你的MVC世界!|前所未有的Web开发探索之旅 衔接上文Spring MVC学习随笔-控制器(Controller)开发详解:控制器跳转与作用域(一) SpingMVC中…...
原型模式(Prototype Pattern)
1 基本概念 1.1 大佬文章 设计模式是什么鬼(原型) 详解设计模式:原型模式-腾讯云开发者社区-腾讯云 1.2 知识汇总 (1)原型模式:先 new 一个实例,该实例符合需求,之后再根据这个实…...
IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket
文章目录 1. 引言2. 短轮询(Short Polling)2.1 原理2.2 代码示例2.2.1 服务器端(Node.js)2.2.2 客户端(HTML JavaScript) 3. 长轮询(Long Polling)3.1 原理3.2 代码示例3.2.1 服务器…...
04_W5500_TCP_Server
上一节我们完成了TCP_Client实验,这节使用W5500作为服务端与TCP客户端进行通信。 目录 1.W5500服务端要做的: 2.代码分析: 3.测试: 1.W5500服务端要做的: 服务端只需要打开socket,然后监听端口即可。 2…...
入门Redis学习总结
记录之前刚学习Redis 的笔记, 主要包括Redis的基本数据结构、Redis 发布订阅机制、Redis 事务、Redis 服务器相关及采用Spring Boot 集成Redis 实现增删改查基本功能 一:常用命令及数据结构 1.Redis 键(key) # 设置key和value 127.0.0.1:6379> set …...
SpringSecurity6 | 自定义登录页面
✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏…...
从单向链表中删除指定值的节点
输入一个单向链表和一个节点的值,从单向链表中删除等于该值的节点,删除后如果链表中无节点则返回空指针。 链表的值不能重复。构造过程,例如输入一行数据为:6 2 1 2 3 2 5 1 4 5 7 2 2则第一个参数6表示输入总共6个节点,第二个参数…...
Vue2与Vue3的语法对比
Vue2与Vue3的语法对比 Vue.js是一款流行的JavaScript框架,通过它可以更加轻松地构建Web用户界面。随着Vue.js的不断发展,Vue2的语法已经在很多应用中得到了广泛应用。而Vue3于2020年正式发布,带来了许多新的特性和改进,同时也带来…...
实时动作识别学习笔记
目录 yowo v2 yowof 判断是在干什么,不能获取细节信息 yowo v2 https://github.com/yjh0410/YOWOv2/blob/master/README_CN.md ModelClipmAPFPSweightYOWOv2-Nano1612.640ckptYOWOv2-Tiny...
5G常用简称
名称缩写全称非周期 信道状态信息参考信号aperidoc CSIAperidoc Channel State Information缓冲区状态报告BSRBuffer Status Report小区特定无线网络标识CS-RNTICell-Specific Radio Network Temporary Identifier主小区组MCGMaster Cell groupMCG的节点MNMasternode主小区PCel…...
自动化测试框架性能测试报告模板
一、项目概述 1.1 编写目的 本次测试报告,为自动化测试框架性能测试总结报告。目的在于总结我们课程所压测的目标系统的性能点、优化历史和可优化方向。 1.2 项目背景 我们公开课的性能测试目标系统。主要是用于我们课程自动化测试框架功能的实现,以及…...
【SpringBoot】解析Springboot事件机制,事件发布和监听
解析Springboot事件机制,事件发布和监听 一、Spring的事件是什么二、使用步骤2.1 依赖处理2.2 定义事件实体类2.3 定义事件监听类2.4 事件发布 三、异步调用3.1 启用异步调用3.2 监听器方法上添加 Async 注解 一、Spring的事件是什么 Spring的事件监听(…...
华为ensp实验——基于全局地址池的DHCP组网实验
目录 前言实验目的实验内容实验结果 前言 该实验基于华为ensp,版本号是1.3.00.100 V100R003C00SPC100,只供学习和参考,不作任何商业用途。 具体的DHCP命令可以看系列文章链接,计算机网络实验(华为eNSP模拟器ÿ…...
如何选择一款安全可靠的跨网安全数据交换系统?
随着网络和数据安全的重视程度增加,为了有效地保护内部的核心数据资产,普遍会采用内外网隔离的策略。像国内的政府机构、金融、能源电力、航空航天、医院等关乎国计民生的行业和领域均已进行了网络的隔离,将内部划分成不同的网段,…...
基于c++版本的数据结构改-python栈和队列思维总结
##栈部分-(叠猫猫) ##抽象数据类型栈的定义:是一种遵循先入后出的逻辑的线性数据结构。 换种方式去理解这种数据结构如果我们在一摞盘子中取到下面的盘子,我们首先要把最上面的盘子依次拿走,才可以继续拿下面的盘子&…...
保姆级教程:在Ubuntu 20.04上搞定Montreal Forced Aligner (MFA) 2.0安装与验证
保姆级教程:在Ubuntu 20.04上搞定Montreal Forced Aligner (MFA) 2.0安装与验证 语音对齐技术正在成为语音处理领域的基础工具,而Montreal Forced Aligner(MFA)作为当前最流行的开源解决方案,其2.0版本带来了显著的性…...
英雄联盟智能工具League Akari:从效率提升到战术优化的全方位解决方案
英雄联盟智能工具League Akari:从效率提升到战术优化的全方位解决方案 【免费下载链接】League-Toolkit An all-in-one toolkit for LeagueClient. Gathering power 🚀. 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit 你是否曾在英…...
H3C IRF 四台交换机堆叠实战:环型拓扑配置详解
1. 四台H3C交换机IRF堆叠入门指南 第一次接触H3C交换机的IRF堆叠功能时,我完全被它的强大所震撼。简单来说,IRF(Intelligent Resilient Framework)技术可以把多台物理交换机虚拟成一台逻辑设备,不仅简化管理ÿ…...
如何永久保存微信聊天记录?WeChatMsg终极指南让你重获数据掌控权
如何永久保存微信聊天记录?WeChatMsg终极指南让你重获数据掌控权 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trendin…...
卡尔曼滤波调参实战:如何用MATLAB让MPU6050的加速度数据更‘听话’?
卡尔曼滤波调参实战:如何用MATLAB让MPU6050的加速度数据更‘听话’? 当你在MATLAB中第一次看到MPU6050的原始加速度数据时,那些疯狂跳动的曲线可能会让你怀疑人生。别担心,这不是传感器坏了,而是现实世界本就充满噪声…...
终极Cinder着色器编程指南:7个GLSL视觉效果开发技巧
终极Cinder着色器编程指南:7个GLSL视觉效果开发技巧 【免费下载链接】Cinder Cinder is a community-developed, free and open source library for professional-quality creative coding in C. 项目地址: https://gitcode.com/gh_mirrors/ci/Cinder Cinder…...
别再用Delay了!用GD32的TIMER5实现精准1ms定时,让你的嵌入式程序更高效
告别阻塞式延时:用GD32 TIMER5构建高效嵌入式系统心跳 在嵌入式开发中,时间管理如同系统的心跳,决定了整个应用的响应速度和执行效率。许多开发者习惯使用delay_ms()这类阻塞式延时函数,却不知这会让CPU陷入无意义的等待状态&…...
Wan2.2-I2V-A14B文生视频模型落地实践:单卡4090D高效推理部署案例
Wan2.2-I2V-A14B文生视频模型落地实践:单卡4090D高效推理部署案例 1. 项目背景与价值 视频内容创作正成为数字时代的重要需求,但传统视频制作流程复杂、成本高昂。Wan2.2-I2V-A14B作为新一代文生视频模型,能够直接将文本描述转化为高质量视…...
AI运维管理与安全防护设备功率MOSFET选型方案——高效、可靠与智能驱动系统设计指南
随着智能化运维与主动安全防护需求的爆发式增长,AI边缘计算节点、智能传感器与安全执行单元已成为现代基础设施管理的核心。其电源管理与信号驱动系统作为设备可靠运行与实时响应的基石,直接决定了系统的能效、稳定性及防护等级。功率MOSFET作为该系统中…...
3分钟搞定加密音乐:Unlock-Music浏览器解密终极指南
3分钟搞定加密音乐:Unlock-Music浏览器解密终极指南 【免费下载链接】unlock-music 在浏览器中解锁加密的音乐文件。原仓库: 1. https://github.com/unlock-music/unlock-music ;2. https://git.unlock-music.dev/um/web 项目地址: https:/…...

















