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

Java创建线程的方式只有一种:Thread+Runnable

Java创建线程的方式其实只有一种

  • 👨‍🎓一、继承Thread
  • 👨‍🎓二、实现Runnable接口
  • 👨‍🎓三、实现Callable接口
  • 👨‍🎓四、通过线程池创建
  • 👨‍🎓五、总结

一般我们会认为创建线程的方式是三到四种,其实 本质上这四种没有任何区别,都是利用Thread+Runnable来进行实现的多线程,其实java自始至终创建多线程的方式都只有一种,下面一起看下他们是怎么对Runnable进行改造成多种多样的。

👨‍🎓一、继承Thread

继承Thread下面是常见的写法

public class Test1 extends Thread{@Overridepublic void run() {while(true){System.out.println("线程1");}}public static void main(String[] args) {new Test1().start();}}

利用这种方式实现的线程,就是继承Thread类然后重写run方法,我们在run方法内部进行线程逻辑的编写。java的start方法会调用start0方法,start0是一个native方法,底层会调用run方法进行执行线程,所以我们是重写run方法。那run方法是怎么来的呢?
在这里插入图片描述
上面是run方法在Thread中的实现,我们可以看到他被注解Override修饰了,说明这个方法是父类的方法,我们点击左侧红色向上的箭头,就会发现跳到了Runnable接口中,如下
在这里插入图片描述
这样就很简单明了了,所以这样就简单明了了,我们在继承Thread重写run方法时其实重写的是Runnable的run方法。这这种已经证明了我们开始说的java是利用Thread+Runnable实现的多线程了

👨‍🎓二、实现Runnable接口

这种方式也是需要依赖Thread才能去创建线程,如下所示

		public class TestThread {public static void main(String[] args) {new Thread(() -> {while(true)System.out.println("Runnable多线程1");}).start();new Thread(() -> {while(true)System.out.println("Runnable多线程2");}).start();}}

那我们来看下这个实现方式到底是如何进行多线程创建的吧,我们还是从run方法开始,因为Thread启动线程时调用的是自己的run方法,那我们就先看下他自己的run方法实现:
在这里插入图片描述
可以看到Thread的run方法调用的是target.run方法,那target是什么呢?ctrl+左会发现,他就是一个Runnable,那这个Runnable怎么来的呢?我们创建过程中只在Thread的构造方法中传入了Runnable,那是不是在这里咱们一起看下:
在这里插入图片描述
这里没有做什么实质的操作,所以继续看,最后调用到了这里:

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name.toCharArray();Thread parent = currentThread();SecurityManager security = System.getSecurityManager();if (g == null) {/* Determine if it's an applet or not *//* If there is a security manager, ask the security managerwhat to do. */if (security != null) {g = security.getThreadGroup();}/* If the security doesn't have a strong opinion of the matteruse the parent thread group. */if (g == null) {g = parent.getThreadGroup();}}/* checkAccess regardless of whether or not threadgroup isexplicitly passed in. */g.checkAccess();/** Do we have the required permissions?*/if (security != null) {if (isCCLOverridden(getClass())) {security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);}}g.addUnstarted();this.group = g;this.daemon = parent.isDaemon();this.priority = parent.getPriority();if (security == null || isCCLOverridden(parent.getClass()))this.contextClassLoader = parent.getContextClassLoader();elsethis.contextClassLoader = parent.contextClassLoader;this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();this.target = target; // 这里是关键点setPriority(priority);if (parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/* Stash the specified stack size in case the VM cares */this.stackSize = stackSize;/* Set thread ID */tid = nextThreadID();}

我们会发现这个方法内部将我们从构造器中传入的Runnable对象放到了this.target中,所以可以看到Thread中的run方法调用的就是我们重写的run方法。所以这个也证明了java中是通过Thread+Runnable实现的多线程方式。

👨‍🎓三、实现Callable接口

这种方式我们需要依赖FutrureTask,其实我们可以接受返回值也得归功FutureTask,下面是常见的实现代码:

public class TestThread {public static void main(String[] args) throws Exception{FutureTask<String> futureTask = new FutureTask<>(()->{int i =0 ;while(i<100)System.out.println("Callable线程1在执行:"+i++);return "线程1执行完了";});FutureTask<String> futureTask2 = new FutureTask<>(()->{int i =0 ;while(i<100)System.out.println("Callable线程2在执行:"+i++);return "线程2执行完了";});new Thread(futureTask).start();new Thread(futureTask2).start();System.out.println(futureTask.get());System.out.println(futureTask2.get());}}

run方法才是多线程的执行地方,还是一样我们还是从run方法开始看,我们点击下new Thread会进入Thread的构造器,发现进入的构造器和第二种是一样的:
在这里插入图片描述
这就说明FutureTask一定实现了或者继承了Runnable接口,其实FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable接口,因为继承和实现的特性,相当于FutureTask实现了Runnable接口。那走到这个构造器就是理所当然那的了。所以与第二种方式相同的是Thread调用的target.run就是FutureTask的run了。我们来看下FutureTask的run方法吧:

public void run() {if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))return;try {Callable<V> c = callable;if (c != null && state == NEW) {V result;boolean ran;try {result = c.call();ran = true;} catch (Throwable ex) {result = null;ran = false;setException(ex);}if (ran)set(result);}} finally {// runner must be non-null until state is settled to// prevent concurrent calls to run()runner = null;// state must be re-read after nulling runner to prevent// leaked interruptsint s = state;if (s >= INTERRUPTING)handlePossibleCancellationInterrupt(s);}}

上面是FutureTask的run方法,里面真正调用的是c.call方法,看到call方法应该就明白了,不错这个call就是我们传入到FutureTask中的Callable实例的call方法,所以FutureTask的调用路线也就清晰了:Thread.start–>Thread.run–>FutureTask.run–>Callable.call。而FutureTask则是Runnable的子类,所以也证明了我们一开始说的java是通过Thread+Runnable来实现的多线程。此外通过这里我们还可以清洗的看到FutureTask是怎么实现参数返回接收的,就是因为call方法有返回,然后FutureTask的run方法接收到返回后将他存放到自身的泛型V中,然后我们就可以直接通过FutureTask.get方法进行获取了。其实这种思想java里有很多,比如常见的http请求的包装类也是通过这种思想来处理流数据的。

👨‍🎓四、通过线程池创建

线程池这里咱们采用自己实现的线程池来进行解释说明,其实没啥区别,只不过是我们一般使用线程池都是禁止直接使用jdk自带线程池的所以才有用自己实现的线程池来看这个问题

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,1, 60,TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy() );threadPoolExecutor.execute(()->{while(true){System.out.println("线程4执行中");}});

关于各个参数的意思,这里就不解释了,需要的看这里:4万字爆肝总结java多线程知识点,言归正传,我们看下线程池是怎么创建线程的,线程池提交任务有submit和execute还有一个定时的schedule,不过schedule他的线程池类不是这个,不过原理一样这里只介绍ThreadPoolExecutor的。我们看下execute的实现方法

public void execute(Runnable command) {if (command == null)throw new NullPointerException();// 此处省略原文注释int c = ctl.get();if (workerCountOf(c) < corePoolSize) {//判断线程是否小于核心线程数,很明显第一次会进入这里if (addWorker(command, true))return;c = ctl.get();}if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();if (! isRunning(recheck) && remove(command))reject(command);else if (workerCountOf(recheck) == 0)addWorker(null, false);}else if (!addWorker(command, false))reject(command);}

很明显第一次执行会执行:addWorker(command, true)这个方法,我们再看下这个方法做了什么

private boolean addWorker(Runnable firstTask, boolean core) {retry:for (;;) {int c = ctl.get();int rs = runStateOf(c);// Check if queue empty only if necessary.if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty()))return false;for (;;) {int wc = workerCountOf(c);if (wc >= CAPACITY ||wc >= (core ? corePoolSize : maximumPoolSize))return false;if (compareAndIncrementWorkerCount(c))break retry;c = ctl.get();  // Re-read ctlif (runStateOf(c) != rs)continue retry;// else CAS failed due to workerCount change; retry inner loop}}boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {w = new Worker(firstTask);//这个firstTask实际就是我们从execute传入的Runnable实现类final Thread t = w.thread;//获取thread,因为线程开启必须利用Thread的start方法,这两步就是最关键的地方if (t != null) {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {// Recheck while holding lock.// Back out on ThreadFactory failure or if// shut down before lock acquired.int rs = runStateOf(ctl.get());if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {if (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();workers.add(w);int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;}} finally {mainLock.unlock();}if (workerAdded) {t.start();//这里真正调用了线程启动workerStarted = true;}}} finally {if (! workerStarted)addWorkerFailed(w);}return workerStarted;}

最关键的两步,笔者在代码中加了注释了,可以看到会将我们传入的Runnable的实现类交给Worker的构造器,那我们看看这个构造器又干了什么:
在这里插入图片描述
可以看到在这个构造器里面,将我们传入的Runnable给到了自己的Runnable,对他的私有变量进行了初始化,然后又对thread进行了初始化,而初始化Thread时传入了this,注意传入的是this。因为Worker实现了Runable,所以当我们真正执行start方法时,调用的应该是Worker的run方法,所以我们到这里应该去看run方法了,run方法很简单,他直接调用了runWorer方法,那看下runWorker的实现:

final void runWorker(Worker w) {Thread wt = Thread.currentThread();Runnable task = w.firstTask;//将Runnable的对象指向一个新的引用w.firstTask = null;//失效原引用w.unlock(); // allow interrupts//加锁,worker实现了AQS,支持锁boolean completedAbruptly = true;try {while (task != null || (task = getTask()) != null) {w.lock();// If pool is stopping, ensure thread is interrupted;// if not, ensure thread is not interrupted.  This// requires a recheck in second case to deal with// shutdownNow race while clearing interruptif ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP))) &&!wt.isInterrupted())wt.interrupt();try {beforeExecute(wt, task);Throwable thrown = null;try {task.run();//这里相当于运行了我们在execute中传入的Runnable对象的run方法了。} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);}} finally {task = null;w.completedTasks++;w.unlock();}}completedAbruptly = false;} finally {processWorkerExit(w, completedAbruptly);}}

看笔者在代码中加的注释很清晰就看到最终调用的是我们从execute中传入的Runnable的对象的run方法。所以我们看到这里也是可以得出一个解决线程池还是利用Thread+Runnable接口一起实现的多线程。那么来从新梳理下线程池的调用过程:
execute(Runnable)–>
addWorker(command, true)–>内部对Worker(Runnale)进行初始化,Worker该类实现了Runnable,初始化Worker时生产线程Thread传入Worker自己,之后获取上一步生产的thread调用start方法。执行start方法,实际执行的是Worker的run方法,worker.run方法调用了runWorker方法,该方法执行时是将Worker的Runnable的对象的run方法进行调用执行。而Worker的Runnable的对象就是在addWorker中由execute方法传入的Runnable实现类。这样整个流程就很清晰了,我们也是可以佐证一开始说的观点了。

👨‍🎓五、总结

看了以上四种分析,我们可以清晰的发现了java中其实创建线程的方式就只有一种就是利用Thread+Runnable来实现多线程。其他多有方式都是对这个实现方式的变种。如有不对欢迎路过的朋友指正,也希望能帮到路过的朋友

在这里插入图片描述

相关文章:

Java创建线程的方式只有一种:Thread+Runnable

Java创建线程的方式其实只有一种&#x1f468;‍&#x1f393;一、继承Thread&#x1f468;‍&#x1f393;二、实现Runnable接口&#x1f468;‍&#x1f393;三、实现Callable接口&#x1f468;‍&#x1f393;四、通过线程池创建&#x1f468;‍&#x1f393;五、总结一般我…...

数据加密--课后程序(Python程序开发案例教程-黑马程序员编著-第3章-课后作业)

实例6&#xff1a;数据加密 数据加密是保存数据的一种方法&#xff0c;它通过加密算法和密钥将数据从明文转换为密文。 假设当前开发的程序中需要对用户的密码进行加密处理&#xff0c;已知用户的密码均为6位数字&#xff0c;其加密规则如下&#xff1a; 获取每个数字的ASCI…...

【GO】K8s 管理系统项目33[前端部分–登录和登出]

K8s 管理系统项目[前端部分–登录和登出] 1. 登录登出流程 1.1 登录流程 登入流程总的分为5步: 账号密码验证token生成token验证验证成功进行跳转验证失败返回/login 1.2 登出流程 登出流程就相对简单,分为2步 删除Token跳转/login 2. 登录代码 src/views/login/Login.v…...

Vue 计算属性基础知识 监听属性watch

计算属性的概念 在{{}}模板中放入太多的逻辑会让模板内容过重且难以维护。例如以下代码&#xff1a; <div id"app">{{msg.split().reverse().join()}}</div><script>const vm new Vue({el: "#app",data: {msg:我想把vue学的细一点}})&…...

PAT:L1-004 计算摄氏温度、L1-005 考试座位号、L1-006 连续因子(C++)

目录 L1-004 计算摄氏温度 问题描述&#xff1a; 实现代码&#xff1a; L1-005 考试座位号 问题描述&#xff1a; 实现代码&#xff1a; 原理思路&#xff1a; L1-006 连续因子 问题描述&#xff1a; 实现代码&#xff1a; 原理思路&#xff1a; 过于简单的就不再写…...

Redis集群方案应该怎么做?

今天我们来跟大家唠一唠JAVA核心技术-RedisRedis是一款流行的内存数据库&#xff0c;适用于高性能的数据缓存和实时数据处理。当需要处理大量数据时&#xff0c;可以使用Redis集群来提高性能和可用性。Redis在单节点模式下&#xff0c;虽然可以支持高并发、快速读写、丰富的数据…...

连续点击返回键退出Android 应用

问题 业务需要&#xff0c;在主界面连续点击返回键退出应用&#xff0c;记录一下。 解决方案 先说结论&#xff0c;在主界面Activity中添加如下代码 /*** 记录上次点击返回键时间*/private long lastClickTime 0;/*** 两次回退点击时间间隔设置不小于2s*/public static fi…...

【PyTorch】教程:torch.nn.Hardswish

torch.nn.Hardswish 原型 CLASS torch.nn.Hardswish(inplaceFalse) 参数 inplace (bool) – 内部运算&#xff0c;默认为 False 定义 Hardswish(x){0if x≤−3,xif x≥3,x⋅(x3)/6otherwise\text{Hardswish}(x) \begin{cases} 0 & \text{if~} x \le -3, \\ x & \te…...

nacos源码入门

nacos官方文档地址&#xff1a;nacos官方文档 Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称&#xff0c;一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 简单来说&#xff0c;nacos就是一个注册中心、配置中心&#xff0…...

【记录】Samba|Windows 11的Samba连接切换用户

Samba是一个用于共享文件和打印机的网络协议&#xff0c;可以使不同的操作系统之间共享文件和资源变得容易。在Windows 11上&#xff0c;可以使用Samba来连接到网络共享。 如果您想在Windows 11上切换用户并连接到另一个Samba共享&#xff0c;可以按照以下步骤操作。 文章目录…...

vue hiprint vue使用hiprint打印控件VUE HiPrint HiPrint简单使用

vue hiprint vue使用hiprint打印控件VUE HiPrint HiPrint简单使用安装相关依赖安装Hi PrintJQuery引入依赖简单使用官方所有 打印示例安装相关依赖 安装Hi Print npm install vue-plugin-hiprintJQuery 因为 hi print 使用到了 JQuery 所以需要安装对应依赖 npm i jquery -…...

HBase常用Shell命令

HBase提供了一个非常方便的命令行交互工具HBase Shell。通过HBase Shell&#xff0c;HBase可以与MySQL命令行一样创建表、索引&#xff0c;也可以增加、删除和修改数据&#xff0c;同时集群的管理、状态查看等也可以通过HBase Shell实现。 一、数据定义语言 数据定义语言&…...

【阿里云】Apsara Clouder云计算专项技能认证-云服务器ECS入门,考试真题分享

以下是阿里云Apsara Clouder云计算专项技能认证-云服务器ECS入门真题汇总篇分享&#xff1a; 1.下列哪一个不是重置ECS密码的步骤? A. 查看实例详情 B.进入控制台 C.远程连接ECS D.点击控制台“概览” 2.针对云服务器ECS安全组说法正确的是 A.是一种物理防火墙 B.仅用于控制…...

怎样编写java程序

搭建好了Java开发环境之后&#xff0c;下面就来学习一下如何开发Java程序。为了让初学者更好地完成第一个Java程序&#xff0c;接下来通过几个步骤进行逐一讲解。 1&#xff0e;编写Java源文件 在D盘根目录下新建一个test文件夹&#xff0c;并在该文件夹中新建文本文档&#…...

面向对象设计模式:结构型模式之适配器模式

一、引入 Object Oriented Adapters 二、XX 模式 aka&#xff1a;Wrapper (包装器) 2.1 Intent 意图 Convert the interface of a class into another interface clients expect. 将一个类的接口转换成客户希望的另外一个接口. 作为两个不兼容的接口之间的桥梁 适配器模式使…...

Unity3D Shader系列之模板测试

一、 模板测试原理模板测试位于GPU渲染流水线的逐片元操作阶段&#xff0c;片元着色器完成之后就会进入模板测试&#xff0c;模板测试通过后再进入深度测试。我们的GPU中有一个模板缓冲区(Stencil Buffer)(Stencil即是模板的意思)&#xff0c;其大小为整个屏幕大小*8位&#xf…...

机器学习中的数学——精确率与召回率

在Yolov5训练完之后会有很多图片&#xff0c;它们的具体含义是什么呢&#xff1f; 通过这篇博客&#xff0c;你将清晰的明白什么是精确率、召回率。这个专栏名为白话机器学习中数学学习笔记&#xff0c;主要是用来分享一下我在 机器学习中的学习笔记及一些感悟&#xff0c;也希…...

Oracle启动数据库报ORA-01102解决办法

1.机器启动之后登录服务器使用sqlplus / as sysdba 登录数据库发现数据库并没有启动之前把数据库服务添加过开机自启动 2.使用startup命令启动数据库报错了 SYSorcl>startup; ORACLE 例程已经启动。 Total System Global Area 2471931904 bytes Fixed Size 2255752 byt…...

Go 语言面向对象编程及实践

面向对象编程是计算机科学中的一种重要的编程方法,它将数据和处理它的代码组合成对象,并将这些对象组合成更大的程序。在 Go 语言中,我们同样可以使用面向对象编程的方式进行开发。本篇文章将介绍 Go 语言面向对象编程的概念、特性、使用方法以及实践技巧。 面向对象编程概…...

0102 MySQL05

1.约束 1.约束&#xff08;constraint&#xff09;&#xff1a;在创建表时&#xff0c;可以给表中的字段加上一些约束&#xff0c;保证表中数据的完整性&#xff0c;有效性 常见的约束&#xff1f; 非空约束&#xff1a;not null 唯一性约束&#xff1a;unique 主键约束&am…...

uniapp 对接腾讯云IM群组成员管理(增删改查)

UniApp 实战&#xff1a;腾讯云IM群组成员管理&#xff08;增删改查&#xff09; 一、前言 在社交类App开发中&#xff0c;群组成员管理是核心功能之一。本文将基于UniApp框架&#xff0c;结合腾讯云IM SDK&#xff0c;详细讲解如何实现群组成员的增删改查全流程。 权限校验…...

Docker 运行 Kafka 带 SASL 认证教程

Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明&#xff1a;server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

Keil 中设置 STM32 Flash 和 RAM 地址详解

文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/

使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题&#xff1a;docker pull 失败 网络不同&#xff0c;需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...

Fabric V2.5 通用溯源系统——增加图片上传与下载功能

fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...

C++.OpenGL (14/64)多光源(Multiple Lights)

多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...

【分享】推荐一些办公小工具

1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由&#xff1a;大部分的转换软件需要收费&#xff0c;要么功能不齐全&#xff0c;而开会员又用不了几次浪费钱&#xff0c;借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别

【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而&#xff0c;传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案&#xff0c;能够实现大范围覆盖并远程采集数据。尽管具备这些优势&#xf…...

苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会

在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...

【Linux】Linux安装并配置RabbitMQ

目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的&#xff0c;需要先安…...