Java进阶篇--线程池之ScheduledThreadPoolExecutor

news2024/12/26 0:19:15

目录

ScheduledThreadPoolExecutor简介

构造方法

特有方法

可周期性执行的任务-ScheduledFutureTask

DelayedWorkQueue

什么是DelayedWorkQueue?

为什么要使用DelayedWorkQueue呢?

DelayedWorkQueue的数据结构

ScheduledThreadPoolExecutor执行过程

总结


ScheduledThreadPoolExecutor简介

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor(关于ThreadPoolExecutor可以看这篇文章),并实现了ScheduledExecutorService接口,因此它同时具备了线程池的基本功能和任务调度的功能。ScheduledThreadPoolExecutor可以用来延迟执行任务或者周期性执行任务,相比于传统的Timer类,其功能更加强大和灵活。

在UML图中,可以看到ScheduledThreadPoolExecutor关联了DelayedWorkQueue和ScheduledFutureTask这两个关键的内部类。DelayedWorkQueue实现了BlockingQueue接口,提供了阻塞队列的功能,用于存储延迟执行的任务。而ScheduledFutureTask继承自FutureTask类,表示异步任务的结果。

ScheduledThreadPoolExecutor的构造函数可以指定后台线程的个数,这使得开发者可以更灵活地控制任务的执行。而ScheduledExecutorService接口定义了延时执行任务和周期执行任务的方法,使得任务调度变得更加简单和便捷。

总之,ScheduledThreadPoolExecutor通过继承ThreadPoolExecutor和实现ScheduledExecutorService接口,结合内部的DelayedWorkQueue和ScheduledFutureTask类,为开发者提供了一个功能强大、灵活可控的定时任务执行框架。这使得在实际开发中,我们可以更加高效地处理定时任务相关的需求。

构造方法

对于ScheduledThreadPoolExecutor的构造方法,可以看出它继承自ThreadPoolExecutor,因此在构造方法中调用了ThreadPoolExecutor的不同重载形式。下面我将对其构造方法进行简要解释:

1、第一个构造方法

// 第一个构造方法
public ScheduledThreadPoolExecutor(int corePoolSize) {
    // 调用ThreadPoolExecutor的构造方法,指定核心线程池大小为corePoolSize,
    // 最大线程数为Integer.MAX_VALUE,空闲线程存活时间为0纳秒(即不保留空闲线程),
    // 使用NANOSECONDS作为时间单位,以及默认的DelayedWorkQueue作为工作队列。
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

这个构造方法指定了核心线程池大小为corePoolSize,并使用了默认的拒绝策略(将任务添加到工作队列中)。最大线程数设为Integer.MAX_VALUE,保证了理论上这是一个大小无界的线程池。

2、第二个构造方法

// 第二个构造方法
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    // 同样调用ThreadPoolExecutor的构造方法,指定核心线程池大小、最大线程数、空闲线程存活时间等参数,
    // 但在这里允许通过ThreadFactory来自定义线程的创建方式。
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory);
}

这个构造方法除了指定了核心线程池大小和最大线程数外,还允许通过ThreadFactory来自定义线程的创建方式。 

3、第三个构造方法

// 第三个构造方法
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   RejectedExecutionHandler handler) {
    // 允许设置拒绝策略,当线程池和工作队列都已满时,会调用指定的拒绝策略来处理新任务。
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), handler);
}

这个构造方法允许设置拒绝策略,当线程池和工作队列都已满时,会调用指定的拒绝策略来处理新任务。 

4、第四个构造方法

// 第四个构造方法
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    // 组合了前两个构造方法的功能,既允许设置线程工厂,又允许设置拒绝策略。
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory, handler);
}

这个构造方法则组合了前两个构造方法的功能,既允许设置线程工厂,又允许设置拒绝策略。

总之,ScheduledThreadPoolExecutor的构造方法提供了多种选择,可以根据实际需求来灵活配置线程池的参数,从而满足不同的调度任务需求。

特有方法

ScheduledThreadPoolExecutor 实现了 ScheduledExecutorService 接口,并且提供了以下特有方法用于延时执行和周期性执行异步任务:

  • schedule(Runnable command, long delay, TimeUnit unit):在给定的延时时间后执行任务,返回一个 ScheduledFuture 对象,通过它可以获取任务的执行情况。
  • <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit):类似于上一个方法,但是接受一个 Callable 对象,可以返回任务的计算结果。
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):按固定的速率执行重复的任务,首次执行需要延时 initialDelay,之后每隔 period 时间执行一次任务。如果任务的执行时间超过了 period,则下一个任务会立即开始执行。
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):与 scheduleAtFixedRate 类似,不同之处在于它是在前一个任务结束后,等待固定的延迟时间后再执行下一个任务。

这些方法使得 ScheduledThreadPoolExecutor 能够灵活地执行延时任务和周期性任务,并允许开发人员根据具体需求来调度异步任务的执行。

可周期性执行的任务-ScheduledFutureTask

ScheduledFutureTask 是 Java 中用于支持周期性执行任务的重要类之一。在 ScheduledThreadPoolExecutor 中,当调用 schedule、scheduleAtFixedRate 和 scheduleWithFixedDelay 方法时,实际上是将提交的任务转换成 ScheduledFutureTask 类的实例。这个转换过程发生在 decorateTask 方法中,其作用是对任务进行装饰和封装,以便进行正确的调度和执行。

在 ScheduledFutureTask 中,重写了 run 方法以支持周期性执行的逻辑。在这个重写的 run 方法中,首先判断当前任务是否是周期性任务。如果不是周期性任务,则直接调用 run 方法来执行任务;如果是周期性任务,则会重新设置下一次执行任务的时间,并将下一次任务放入到延迟队列中,以便在指定的时间再次执行。

因此,ScheduledFutureTask 的主要功能是根据任务的周期性特性,对任务进行进一步封装和调度。对于非周期性任务,它直接执行 run 方法;对于周期性任务,则在每次执行完后重新设置下一次执行的时间,并将下一次任务继续放入到延迟队列中。

总的来说,ScheduledFutureTask 在实现周期性任务调度时起到了关键的作用,能够有效地管理和执行周期性任务。这样的设计使得 ScheduledThreadPoolExecutor 能够将任务和线程进行解耦,实现了任务的延时执行和周期性执行的功能。

一个小例子

import java.util.concurrent.*;

public class main {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1);

        // 创建一个周期性执行的任务
        Runnable periodicTask = new Runnable() {
            private long interval = 1000; // 初始执行间隔为1秒

            @Override
            public void run() {
                System.out.println("在执行定期任务 " + System.currentTimeMillis() + " 带间隔 " + interval);
                if (interval != 3000) {
                    interval += 1000; // 每次执行后增加1秒的执行间隔,直到达到3秒
                    rescheduleTask(this, interval, TimeUnit.MILLISECONDS); // 重新调度任务
                }
            }
        };

        // 使用 schedule 方法提交任务,并获得 ScheduledFuture 实例
        ScheduledFuture<?> scheduledFuture = scheduler.schedule(periodicTask, 1, TimeUnit.SECONDS);

        // 关闭 scheduler
        scheduler.shutdown();
    }

    // 动态重新调度任务的方法
    private static void rescheduleTask(Runnable task, long delay, TimeUnit unit) {
        ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);
        executor.schedule(task, delay, unit);
        executor.shutdown();
    }
}

在这个示例中,我们首先创建了一个 ScheduledThreadPoolExecutor 实例 scheduler,然后定义了一个周期性执行的任务 periodicTask。在任务执行时,我们动态地改变了执行间隔,通过调用 rescheduleTask 方法重新调度任务。rescheduleTask 方法中,我们新建了一个临时的 ScheduledThreadPoolExecutor 实例,用于重新调度任务。

通过这个示例,我们展示了如何在运行时动态地改变周期性任务的执行间隔,并且演示了任务是如何被转换成 ScheduledFutureTask 实例,并且如何进行动态调度的。

DelayedWorkQueue

什么是DelayedWorkQueue?

DelayedWorkQueue 在 ScheduledThreadPoolExecutor 中扮演着重要的角色,它是用来存储需要延迟执行或周期执行的任务的数据结构。DelayedWorkQueue 的实现基于堆的数据结构,类似于 DelayQueue 和 PriorityQueue。

在执行定时任务时,每个任务的执行时间各不相同,因此 DelayedWorkQueue 的工作是按照执行时间的升序排列这些任务,确保执行时间距离当前时间越近的任务排在队列的前面。这样,线程池中的工作线程在获取任务时会优先选择执行时间最近的任务,从而实现了延迟执行和周期执行任务的功能。

DelayedWorkQueue 内部实际上使用了一个数组来存储任务,并通过堆的数据结构对任务按照执行时间进行排序。这种设计使得 ScheduledThreadPoolExecutor 能够高效地管理延迟任务,并按照预定的顺序和时间执行这些任务,从而满足异步任务和周期性任务的执行需求。

综上所述,DelayedWorkQueue 在 ScheduledThreadPoolExecutor 中扮演着关键的角色,通过其基于堆的数据结构和按照执行时间排序的机制,能够确保任务按照预期得到执行,为延迟执行和周期执行任务提供了可靠的支持。

为什么要使用DelayedWorkQueue呢?

使用 DelayedWorkQueue 的主要原因是确保定时任务能够按照预定的执行时间顺利进行。DelayedWorkQueue 本质上是一个优先级队列,它能够保证每次出队的任务都是当前队列中执行时间最靠前的,这正是它在 ScheduledThreadPoolExecutor 中的作用所在。

由于 DelayedWorkQueue 基于堆结构实现,在执行插入和删除操作时的时间复杂度为 O(logN),这使得它能够高效地管理延迟任务,并且保证任务按时执行。通过堆结构的特性,DelayedWorkQueue 能够快速找到最近要执行的任务,并将其置于队列的最前面,从而保证任务能够按时得到执行。

综上所述,DelayedWorkQueue 作为基于堆结构的优先级队列,能够有效地管理延迟任务,并确保任务按照预定的执行时间顺利进行。这种设计使得 ScheduledThreadPoolExecutor 能够高效地调度和执行定时任务,提高系统的可维护性和性能。

DelayedWorkQueue的数据结构

DelayedWorkQueue 的数据结构是基于数组构成的,其中数组元素类型为实现了 RunnableScheduledFuture 接口的类(实际上是 ScheduledFutureTask)。在具体实现中,DelayedWorkQueue 使用一个大小为 16 的数组来存储任务,初始大小定义如下:

// 初始容量为16
private static final int INITIAL_CAPACITY = 16;

// 使用数组作为存储结构,初始大小为INITIAL_CAPACITY
private RunnableScheduledFuture<?>[] queue = new RunnableScheduledFuture<?>[INITIAL_CAPACITY]; 

// 用于在多线程环境下对队列进行加锁操作的ReentrantLock对象
private final ReentrantLock lock = new ReentrantLock(); 

// 追踪队列中实际存储的元素个数
private int size = 0; 

这意味着 DelayedWorkQueue 内部使用一个固定大小的数组来管理任务。当任务需要执行时,会根据任务的延迟时间将其放入数组中适当的位置,以保持任务按照执行时间顺序进行排序。这样,待执行时间越近的任务会被放置在队列的前面,以便最先执行。

总的来说,DelayedWorkQueue 的数据结构是基于数组构成的,通过数组来实现对延迟任务的管理和排序,以确保任务能够按照预定的执行时间顺利进行。

ScheduledThreadPoolExecutor执行过程

ScheduledThreadPoolExecutor 的执行过程如下:

1、任务提交:调用 schedule() 方法提交一个任务,该方法将任务转换成 ScheduledFutureTask 对象。

2、延时执行:在 schedule() 方法中调用 delayedExecute() 方法,将任务放入阻塞队列中进行调度。如果线程池已经关闭,则拒绝任务;否则将任务加入到阻塞队列中。具体源码为:

public ScheduledFuture<?> schedule(Runnable command,
                                   long delay,
                                   TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
	//将提交的任务转换成ScheduledFutureTask
    RunnableScheduledFuture<?> t = decorateTask(command,
        new ScheduledFutureTask<Void>(command, null,
                                      triggerTime(delay, unit)));
    //延时执行任务ScheduledFutureTask
	delayedExecute(t);
    return t;
}

3、确保线程启动:在 delayedExecute() 方法中会调用 ensurePrestart() 方法,该方法的主要逻辑是确保至少有一个线程处于启动状态,即使核心线程数为 0。在 ensurePrestart() 方法中会调用 addWorker() 方法来添加新的 Worker 线程。

private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
		//如果当前线程池已经关闭,则拒绝任务
        reject(task);
    else {
		//将任务放入阻塞队列中
        super.getQueue().add(task);
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        else
			//保证至少有一个线程启动,即使corePoolSize=0
            ensurePrestart();
    }
}

4、Worker 线程执行任务:当 Worker 线程启动时,它会不断地从阻塞队列中获取任务并执行,直到获取的任务为 null,此时线程结束终止。

5、当需要执行周期性任务时,Worker 线程在执行完当前任务后会重新计算下一次任务的执行时间,并将任务重新放入阻塞队列,以便下一次执行。

void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}

通过上述步骤,ScheduledThreadPoolExecutor 能够按照预定的时间执行任务,并且能够确保至少有一个线程处于启动状态,以便执行任务。整个流程涉及任务的转换、延时执行、线程池状态的检查、线程的启动和任务的执行等关键步骤。注意:addWorker方法是ThreadPoolExecutor类中的方法,对ThreadPoolExecutor的源码分析可以看这篇文章,很详细。

总结

ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,因此在整体功能上保持一致。线程池的主要职责是创建线程(Worker类),而线程则不断地从阻塞队列中获取新的异步任务,直到队列中没有任务为止。相较于ThreadPoolExecutor,ScheduledThreadPoolExecutor具有延时执行任务和定期执行任务的能力。它重新设计了任务类ScheduleFutureTask,并重写了run方法以实现延时执行和周期性执行任务。此外,它使用了DelayedWorkQueue作为阻塞队列,这是一个可根据优先级排序的队列,采用了堆的底层数据结构,使得与当前时间相比,待执行时间越接近的任务被放置在队列的前面,以便线程优先获取并执行这些任务。

在设计线程池时,无论是ThreadPoolExecutor还是ScheduledThreadPoolExecutor,都将任务、执行者和任务结果进行了解耦。执行者的任务执行机制完全交由Worker类负责,任务提交后首先进入阻塞队列,然后通过addWork方法创建Worker类,并通过runWorker方法启动线程,不断地从阻塞队列中获取异步任务执行,直至队列为空为止。任务指的是实现了Runnable接口和Callable接口的实现类,在ThreadPoolExecutor中会将任务转换成FutureTask类,而在ScheduledThreadPoolExecutor中,为了实现延时执行和周期性执行任务的特性,任务会被转换成ScheduledFutureTask类,该类继承了FutureTask,并重写了run方法。提交任务后,可以通过Future接口的类获取任务结果,在ThreadPoolExecutor中是FutureTask类,而在ScheduledThreadPoolExecutor中是ScheduledFutureTask类。

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

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

相关文章

uniApp页面通讯

Uniapp 是一款基于 Vue.js 开发的框架&#xff0c;它可以用来开发多端应用&#xff0c;包括微信小程序、H5、APP 等。在 Uniapp 中&#xff0c;页面通讯分为三种方式&#xff1a;事件总线、Vuex 和 uni.$emit。 事件总线&#xff08;EventBus&#xff09;&#xff1a;事件总线是…

挖掘非结构化数据潜能——向量数据库的探索之路

“ 摸着石头过河&#xff0c;一直向前&#xff0c;不断尝试 ” 整理 | 小白 出品&#xff5c;极新 IDC 预测&#xff0c;到 2025 年&#xff0c;中国的数据量将增长到 48.6ZB&#xff0c;80% 是非结构化数据&#xff0c;并且将成为全球最大的数据圈。在我们的日常生活中&…

rancher或者其他容器平台使用非root用户启动jar

场景&#xff1a; java程序打成镜像&#xff0c;在rancher上运行&#xff0c;默认是root账户&#xff0c;发现hdfs或者hive不允许root账户操作&#xff1b;所以打算用费root账户启动jar&#xff0c;使其具有hive和hdfs的操作权限。 Dockerfile entrypoint.sh 思路就是上面这样…

git and svn 行尾风格配置强制为lf

git CLI配置&#xff1a; // 提交时转换为LF&#xff0c;检出时转换为CRLF git config --global core.autocrlf true // 提交时转换为LF&#xff0c;检出时不转换 git config --global core.autocrlf input // 提交检出均不转换 git config --global core.autocrlf f…

C# wpf 实现任意控件(包括窗口)更多拖动功能

系列文章目录 第一章 Grid内控件拖动 第二章 Canvas内控件拖动 第三章 任意控件拖动 第四章 窗口拖动 第五章 附加属性实现任意拖动 第六章 拓展更多拖动功能&#xff08;本章&#xff09; 文章目录 系列文章目录前言一、添加的功能1、任意控件MoveTo2、任意控件DragMove3、边…

19 款Agent产品工具合集

原文&#xff1a;19 款Agent产品工具合集 什么是Agent? 你告诉GPT完成一项任务&#xff0c;它就会完成一项任务。 如果你不想为GPT提出所有任务怎么办&#xff1f;如果你想让GPT自己思考怎么办&#xff1f; 想象一下&#xff0c;你创建了一个AI&#xff0c;你可以给它一个…

第一章:IDEA

系列文章目录 文章目录 系列文章目录前言一、IDEA 的使用1.1 IDEA 工作界面1.2 IDEA 的基本介绍和使用1.3 IDEA 使用技巧和经验1.4 IDEA编译与源文件1.5 IDEA 常用快捷键1.6 IDEA模板/自定义模板 总结 前言 IDEA 全称 IntelliJ IDEA&#xff0c;在业界被公认为最好的 Java 开发…

C++进阶-模板

模板 模板的概念函数模板函数模板语法函数模板注意事项案例-实现数据的排序函数模板与普通函数的区别普通函数与函数模板的调用规则 模板的局限性类模板的基本语法类模板与函数模板的区别类模板中成员函数创建时机类模板对象做函数参数类模板与继承类模板成员函数类外实现类模板…

模拟量指令

这里写自定义目录标题 模拟量scale指令导入模拟量输入原理硬件组态 指令运用信号发生器使用S_ITR(integer to real) 整数转换浮点数 模拟量输入信号输出信号标准信号非标准信号RTD&#xff08;Resistance Temperature Detector&#xff0c;热电阻&#xff09;实物图接线方法 TC…

顶板事故防治vr实景交互体验提高操作人员安全防护技能水平

建筑业在我国各行业中属危险性较大且事故多发的行业&#xff0c;在建筑业“八大伤害”(高处坠落、坍塌、物体打击、触电、起重伤害、机械伤害、火灾爆炸及其他伤害)事故中&#xff0c;高处坠落事故的发生率最高、危险性极大。工地现场培训vr坠落体验利用虚拟现实技术还原各种情…

数据结构(c语言版) 栈

顺序栈 要求&#xff1a;实现顺序栈的入栈&#xff0c;出栈&#xff0c;显示栈 代码 #include <stdio.h> #define MAXSIZE 100struct liststack{int data[MAXSIZE];int top; };//初始化栈 void init(struct liststack * LS){LS->top -1; }//入栈操作 void instack…

小程序制作(超详解!!!)第十四节 计时器

1.案例描述 设计一个实现倒计时功能的小程序&#xff0c;小程序运行后&#xff0c;首先显示空白界面&#xff0c;过2秒后才显示计时界面点击“开始计时”按钮后开始倒计时&#xff0c;点击“停止计时”按钮后停止计时。 2.index.wxml <view class"box" hidden&…

Docker安装、卸载,以及各种操作

docker是一个软件&#xff0c;是一个运行与linux和windows上的软件&#xff0c;用于创建、管理和编排容器&#xff1b;docker平台就是一个软件集装箱化平台&#xff0c;是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中&#xf…

服务器数据恢复—云服务器mysql数据库表被truncate的数据恢复案例

云服务器数据恢复环境&#xff1a; 阿里云ECS网站服务器&#xff0c;linux操作系统mysql数据库。 云服务器故障&#xff1a; 在执行数据库版本更新测试时&#xff0c;在生产库误执行了本来应该在测试库执行的sql脚本&#xff0c;导致生产库部分表被truncate&#xff0c;还有部…

【达梦数据库】mysql与达梦整数类型对比关系

最近遇了mysql 和达梦整数类型的数据范围对比&#xff0c;做了个表格供大家分享 对比表格 要说明的是我整理的时候&#xff0c;达梦貌似没有无符号整数类型&#xff08;防杠保护&#xff09;&#xff0c;也就是只能将mysql/dm 的有符号整数类型的的范围值进行对比 MYSQL - t…

C#开源项目:私有化部署LLama推理大模型

推荐一个C#大模型推理开源项目&#xff0c;让你轻松驾驭私有化部署&#xff01; 01 项目简介 LLama是Meta发布的一个免费开源的大模型&#xff0c;是一个有着上百亿数量级参数的大语言模型&#xff0c;支持CPU和GPU两种方式。 而LLamaSharp就是针对llama.cpp封装的C#版本&am…

【数学】 4、向量的内积、外积、模长

文章目录 一、向量点乘&#xff08;内积&#xff09;1.1 几何意义1.2 点乘的代数定义&#xff0c;推导几何定义&#xff08;用于求向量夹角&#xff09;1.2.1 余弦定理 1.3 程序计算 二、向量叉乘&#xff08;外积&#xff09;2.1 几何意义 三、通俗理解内积和外积四、向量的模…

kubernetes集群编排——k8s调度

nodename vim nodename.yaml apiVersion: v1 kind: Pod metadata:name: nginxlabels:app: nginxspec:containers:- name: nginximage: nginxnodeName: k8s2 nodeName: k8s2 #找不到节点pod会出现pending&#xff0c;优先级最高 kubectl apply -f nodename.yamlkubectl get pod …

AI系统ChatGPT程序源码+AI绘画系统源码+支持GPT4.0+Midjourney绘画+已支持OpenAI GPT全模型+国内AI全模型

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…

VMware创建Linux虚拟机之(三)Hadoop安装与配置及搭建集群

Hello&#xff0c;world&#xff01; &#x1f412;本篇博客使用到的工具有&#xff1a;VMware16 &#xff0c;Xftp7 若不熟悉操作命令&#xff0c;推荐使用带GUI页面的CentOS7虚拟机 我将使用带GUI页面的虚拟机演示 虚拟机&#xff08;Virtual Machine&#xff09; 指通过…