⛳ Java多线程 一,线程基础

news/2025/3/26 15:44:18/

线程基础

  • ⛳ Java多线程 一,线程基础
  • 🐾 一,线程基础
      • 💭 1.1,什么是程序,进程,线程
      • 🏭 1.2,什么是并行和并发
      • 👣 1.3,线程使用的场景
      • 🎨 1.4,创建线程
        • 1.4.1,继承 `Thread`类的方式
        • 1.4.2,实现`Runanble`接口:
        • 1.4.3,`Thread` 常见方法
        • 1.4.4,`sleep()`方法
        • 1.4.5,线程优先级
        • 1.4.6,守护线程
        • 1.4.7,线程合并
        • 1.4.8,线程退出
      • 📢 1.5,线程的生命周期
        • 1.5.1,线程的调度与时间片
        • 1.5.2,线程状态
        • 1.5.3,多线程自增 i++ 和线程执行原理

⛳ Java多线程 一,线程基础

当涉及多线程编程时,我们可以同时执行多个任务,从而提高程序的性能和响应性。然而,多线程编程也带来了一些挑战,比如线程同步和资源共享。在本教程中,我们将介绍Java中的多线程编程,包括创建和管理线程,线程同步,以及常见的多线程问题和解决方案。

🐾 一,线程基础

💭 1.1,什么是程序,进程,线程

在计算机科学中,程序(Program),进程(Process)和线程(Thread)是三个重要的概念,它们都与执行计算机任务和程序相关。

  1. 程序(Program):

    程序是一系列指令的集合,这些指令按照特定的顺序组织,用于完成特定的任务或执行特定的操作。程序是静态的,它们只是存储在计算机磁盘或存储设备中,并不直接执行。当我们想要运行一个程序时,操作系统会将程序加载到内存中,并将其转换为进程,然后才能执行其中的指令。

  2. 进程(Process):

    进程是计算机程序在执行时的实例。它是计算机中正在运行的程序的活动副本。每个进程都有自己独立的内存空间,包含程序代码、数据、堆栈等信息。进程之间相互隔离,一个进程的崩溃不会影响其他进程的稳定性。每个进程在操作系统中都有自己的标识符(Process ID),它可以用来管理和监控进程的状态。

    进程有以下特点:

    • 进程之间相互隔离,一个进程的崩溃不会影响其他进程的稳定性。每个进程在操作系统中都有自己的标识符(Process ID),它可以用来管理和监控进程的状态。
    • 资源分配:每个进程拥有自己的内存空间和系统资源。
    • 切换开销:在进程之间切换会导致一定的开销,因为需要保存和恢复各个进程的上下文。
    1. 线程(Thread):

      线程是进程中的执行单元,每个进程可以包含多个线程。线程与进程共享相同的内存空间,因此它们可以更轻松地相互通信。多线程在多核处理器上能够实现并行执行,从而提高程序的性能和响应性。

      线程有以下特点:

      • 共享资源:线程之间共享进程的内存和资源,可以更方便地交换数据和信息。

      • 轻量级:相较于进程,线程的创建、切换和销毁开销较小。

      • 同步问题:多线程共享资源可能导致同步问题,需要采取同步机制来避免数据冲突。

        总结:

        程序是一组指令的集合,进程是程序在执行时的实例,而线程是进程中的执行单元。进程之间相互独立,而线程共享同一进程的资源。多线程在并行处理和提高程序性能方面具有优势,但同时也需要注意处理线程同步问题。

🏭 1.2,什么是并行和并发

并行(Parallel /ˈpærəlel/ )和并发(Concurrent /kənˈkʌrənt/)是两个与计算机执行任务相关的重要概念。

  1. 并行(Parallel):

    并行是指同时执行多个任务或操作。在计算机中,当多个处理器核心或计算单元同时执行不同的任务或同一个任务的不同部分时,我们称之为并行执行。这样可以显著提高计算机系统的处理能力和效率,特别时在多核处理器上。

    举例来说,如果有一个任务要处理一系列的数据,那么在并行处理中,不同的处理器核心可以同时处理这些数据的不同部分,从而更快地完成任务。

  2. 并发(Concurrent):

    并发是指同时处理多个任务,但并不一定是同时执行这些任务。在计算机中,并发执行是通过快速的任务切换和调度来实现的,操作系统会以非常小的时间片轮流执行多个任务,让它们看起来是同时进行的。

    举例来说,如果有两个任务要处理,操作系统可以分别给它们分配时间片,让它们交替执行,从用户的角度来看,好像这两个任务在同时进行。

    关键区别: 并行是真正的同时执行多个任务,需要多个处理器核心或计算单元支持。而并发是通过快速的切换和调度,让多个任务交替执行,从用户的角度来看,好像这些任务在同时进行,但实际上是在时间上错开的。

    总结:

    并行是真正的同时执行多个任务,利用多核处理器等技术提高计算效率;而并发是通过任务切换和调度,让多个任务交替执行,实现任务间的快速切换,从用户角度看起来是同时执行的。并行适合利用多核处理器等资源来加速计算,而并发适合在单核处理器上实现任务的高效利用。

👣 1.3,线程使用的场景

线程在计算机编程和软件开发中有许多使用场景。

它们主要用于以下情况:

  1. 并发执行:线程使得程序能够同时处理多个任务,特别是在多核处理器上能够实现真正的并行执行。例如,一个计算密集型的应用程序可以将任务分成多个线程,在多核处理器上同时执行,提高整体性能。
  2. 多任务处理:线程可以用于在单个应用程序内执行多个任务,使得程序能够同时响应多个用户请求或事件。例如,一个服务器应用程序可以使用多线程来同时处理多个客户端请求。
  3. 用户界面响应:在图形用户界面(GUI)应用程序中,线程用于保持用户界面的响应性。将耗时的任务(如文件加载、网络请求等)放在后台线程中执行,以免阻塞用户界面的操作。
  4. 并行算法:在某些计算密集型任务中,可以使用线程实现并行算法,将计算任务分成多个部分并在不同线程中并行执行,从而加快计算速度。
  5. 服务器应用:在服务器端应用程序中,线程用于同时处理多个客户端连接请求,实现高并发处理能力。
  6. 多媒体处理:在音频、视频处理等应用中,线程可用于同时处理多个媒体流,实现流畅的播放和录制。
  7. 游戏开发:在游戏开发中,线程通常用于同时处理游戏逻辑、图形渲染和用户输入,以提供流畅的游戏体验。

需要注意的是,虽然线程在很多情况下能够提高程序的性能和响应性,但多线程编程也带来了一些挑战,如数据同步与竞争条件。正确地处理线程同步和资源共享问题对于编写稳定和可靠的多线程应用程序至关重要。因此,在使用线程时,开发人员需要仔细考虑并采取适当的同步措施,以避免潜在的问题。

🎨 1.4,创建线程

有两种方式可以创建线程:

1,继承Thread 类并重写 run()方法;

2,实现Runnable接口;

1.4.1,继承 Thread类的方式

  1. 定义子类继承Thread类。
  2. 类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。

案例:

启动一个线程,在线程中执行1-10000的偶数打印工作:

public class MyThread extends Thread {@Overridepublic void run() {for (int i = 1; i <= 10000; i++) {if (i % 2 == 0) {//Thread.currentThread().getName():得到线程的名字System.out.println(Thread.currentThread().getName() + "\t" + i);}}}
}

测试:

public class Test1 {public static void main(String[] args) {MyThread myThread1 = new MyThread();myThread1.start();MyThread myThread2 = new MyThread();myThread2.start();System.out.println(Thread.currentThread().getName() + "  main 线程 over");}
}
        JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞System.out.println("main over");
  • 如果子线程执行,进程不会停止。

1.4.2,实现Runanble接口:

  • 定义子类,实现Runnable接口。
  • 类中重写Runnable接口中的run方法。
  • 通过Thread类含参构造器创建线程对象。
  • 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  • 调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法。
public class ThreadDemo {public static void main(String[] args) {//方式1:Thread子类,启动MyThread thread1 = new MyThread();thread1.start();//方式2:Runable方式(推荐的方式)//优势:可以实现多继承,比如继承BaseDao,然后再实现RunnableMyTask task = new MyTask();Thread thread2 = new Thread(task);thread2.start();//方式3:匿名内部类new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i <= 100; i++) {System.out.println(Thread.currentThread().getName() +  "\t" +  i);}}}).start();}}//方式1
class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i <= 100; i++) {System.out.println(Thread.currentThread().getName() +  "\t" +  i);}}
}//方式2
//优势:可以实现多继承,比如继承BaseDao,然后再实现Runnable
class MyTask implements Runnable {@Overridepublic void run() {for (int i = 0; i <= 100; i++) {System.out.println(Thread.currentThread().getName() +  "\t" +  i);}}
}

1.4.3,Thread 常见方法

  • 构造函数

    • Thread(): 创建新的Thread对象
    • Thread(String threadname): 创建线程并指定线程实例名
    • Thread(Runnable target): 指定创建线程的目标对象,它实现了Runnable接口中的run方法
    • Thread(Runnable target, String name): 创建新的Thread对象
  • void start(): 启动线程,并执行对象的run()方法

  • run(): 线程在被调度时执行的操作

  • String getName(): 返回线程的名称

  • **void setName(String name)😗*设置该线程名称

  • static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类

  • static void yield(): 线程让步

    • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    • 若队列中没有同优先级的线程,忽略此方法
  • join() : 当某个程序执行流中调用其他线程的 join() 方法时, 调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止

  • static void sleep(long millis): (指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。

  • stop(): 强制线程生命期结束,不推荐使用

  • boolean isAlive(): 返回boolean,判断线程是否还活着

1.4.4,sleep()方法

指定线程休眠的时间,单位毫秒,让出cpu时间片,其他线程可以抢占时间片

public class MyTask implements Runnable {@Overridepublic void run() {for (int i = 1; i <= 100; i++) {if (i % 2 == 0) {//Thread.currentThread().getName():得到线程的名字System.out.println(Thread.currentThread().getName() + "\t" + i);try {Thread.sleep(1000);Thread.yield();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}

1.4.5,线程优先级

  • 线程的优先级等级

    • MAX_PRIORITY: 10
    • MIN _PRIORITY: 1
    • NORM_PRIORITY: 5
    public static void main(String[] args) {MyTask myTask = new MyTask();Thread thread1 = new Thread(myTask, "t1");thread1.setPriority(Thread.MIN_PRIORITY);thread1.start();Thread thread2 = new Thread(myTask, "t2");thread1.setPriority(Thread.MAX_PRIORITY);thread2.start();}

1.4.6,守护线程

  • 其他线程都执行结束,守护线程自动结束
  • 守护启动子线程,也是守护线程
  • 守护线程的语法thread.(*setDaemon(true)*设置守护线程
public class Test2 {public static void main(String[] args) {MyThread myThread1 = new MyThread();
//        myThread1.setDaemon(true);myThread1.start(); //守护线程, gc线程,jvm线程结束gc会自动结束JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞System.out.println("main over");}}class MyThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("a");}}
}

1.4.7,线程合并

image-20230731101929769

案例:

public class Test1 {/*** CountDownLatch:可以实现相同的效果* @param args*/public static void main(String[] args) {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 1; i <= 100; i++) {System.out.println(Thread.currentThread().getName() + "\t" + i);try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t1.start();try {t1.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() +  "  main orver");}}

image-20230731102014846

1.4.8,线程退出

  • stop():不推荐,线程退出方式粗暴,不管线程正在执行的任务,直接退出,可能丢失数据。

    image-20230731102216532

    public class Test1 {public static void main(String[] args) {Thread t1 = new Thread(new MyTask());t1.start();Scanner in = new Scanner(System.in);System.out.println("输入1/0:0表示退出");int i = in.nextInt(); ///主线程进入IO阻塞if (i == 0) {t1.stop();}System.out.println("main over");}static class MyTask implements Runnable {@Overridepublic void run() {while (true) {try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
    }
    
  • 中断信号interrupt

    image-20230731102257699

    • interrupt():发送中断信号(true)

      • 如果线程在阻塞状态,比如sleep(),join(),wait(),这时接收到中断信号会抛出一个异常InterruptException,同时中断信号清除(false)
      • 只是发送信号,不会对线程产生影响
    • **static interrupted():**得到中断信号(true),然后把中断信号设置成false

    • **isInterrupted():**得到中断信号,不会清除中断信号

    public class Test1 {public static void main(String[] args) {Thread t1 = new Thread(new MyTask());t1.start();JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞t1.interrupt(); //发送中断信号给t1System.out.println("main over");}static class MyTask implements Runnable {@Overridepublic void run() {while (true) {System.out.println("a");try {Thread.sleep(500000);} catch (InterruptedException e) {System.out.println("bbbbbbbbbbbbbbb");e.printStackTrace();Thread.currentThread().interrupt(); //再次发送中断信号,中断信号发给阻塞线程,抛出Interrupt异常,中断信号清除
    //                    throw new RuntimeException(e);}//得到中断信号,优雅的退出if (Thread.interrupted()) {break;}}}}
    }
    

📢 1.5,线程的生命周期

1.5.1,线程的调度与时间片

​ 由于 CPU 的计算频率非常高,每秒计算数十亿次,于是,可以将 CPU 的时间从毫秒的维度进行分段,每一小段叫做一个 CPU 时间片。不同的操作系统、不同的处理器,线程的 CPU 时间片长度都不同。假定操作系统的线程一个时间片的时间长度为 20 毫秒(比如 Windows XP),在一个 2GHz 的 CPU 上,那么一个时间片可以进行计算的次数是: 20 亿/(1000/20) =4 千万次,也就是说,一个时间片内的计算量是非常巨大的。 目前操作系统中主流的线程调度方式大都是:基于 CPU 时间片方式进行线程调度。线程只有得到 CPU 时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”。线程的调度模型,目前主要分为两种调度模型:分时调度模型、抢占式调度模型。

(1)分时调度模型——系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程人人平等。

下图就是一个分时调度的简单例子:三个线程,轮流得到 CPU 时间片;一个线程执行时,另外两个线程处于就绪状态

image-20230731102854819

(2)抢占式调度模型——系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。 由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。 Java 的线程管理和调度是委托给了操作系统完成的,与之相对应, Java 的线程调度也是使用抢占式调度模型

1.5.2,线程状态

操作系统,线程的状态

image-20230731103051812

java的线程状态

得到java的线程状态

public Thread.State getState(); //返回当前线程的执行状态,一个枚举类型值

Thread.State是一个内部枚举类,定义了6个枚举常量,分别代表Java线程的6种抓过你太,具体如下:

public static enum State {NEW, //新建RUNNABLE, //可执行:包含操作系统的就绪、运行两种状态BLOCKED, //阻塞  -> 操作系统线程中的阻塞WAITING, //等待  -> 操作系统线程中的阻塞TIMED_WAITING, //计时等待 -> 操作系统线程中的阻塞TERMINATED; //终止}

Thread.State定义的6中状态中,有四种是比较常见的状态,他们是:NEW状态,RUNNABLE状态,TERMINATED状态,TIMED_WAITING状态。

接下来,将线程的6种状态以及各种状态的进入条件,做个总结:

  1. NEW状态:通过 new Thread(...)已经创建线程,但尚未调用start()启动线程,该线程处于NEW(新建)状态。虽然前面介绍了4种创建线程,但是其中的其他三种方式,本质上都是通过new Thread()创建的线程,仅仅是创建了不同的target执行目标示例(如Runnable实例)。

  2. RUNNABLE状态:Java 把就绪(Ready)和 执行(Running)两种状态合并为一种状态:可执行(RUNNABLE)状态(或者可运行状态)。调用了线程的start()实例方法后,线程就处于就绪状态了;此线程获取到CPU时间片后,开始执行run()方法种的业务代码,线程处于执行状态。

    image-20230731142105785

    • 就绪状态:就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远是就绪状态;当前线程进入就绪状态的条件,大致包括以下几种:

      • 调用线程的start()方法,此线程进入就绪状态。
      • 当前线程的执行时间片用完。
      • 线程睡眠(sleep)操作结束。
      • 对其他线程合入(join)操作结束。
      • 等待用户输入结束。
      • 线程争抢到对象锁(Object Monitor)。
      • 当前线程调用了yield()方法,让出CPU执行权限。
    • 执行状态:线程调度程序从就绪状态的线程中选择一个线程,作为当前线程时线程所处的状态。这也是线程进入执行状态的唯一方式。

    • BLOCKED 状态: 处于阻塞(BLOCKED)状态的线程并不会占用CPU资源,以下情况会让线程进入阻塞状态:

      1. 线程等待获取锁 :等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
      2. IO 阻塞:线程发起了一个阻塞式 IO 操作后,如果不具备 IO 操作的条件,线程会进入阻塞状态。 IO 包括磁盘 IO、 网络 IO 等。 IO 阻塞的一个简单例子:线程等待用户输入内容后继续执行。
    • WAITING 状态:处于 WAITING(无限期等待)状态的线程不会被分配 CPU 时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下 3 种方法,会让自己进入无限等待状态:

      1. Object.wait() 方法,对应的唤醒方式为: Object.notify() / Object.notifyAll()。
      2. Thread.join() 方法,对应的唤醒方式为:被合入的线程执行完毕。
      3. LockSupport.park() 方法,对应的唤醒方式为: LockSupport.unpark(Thread)。
    • TIMED_WAITING 状态:处于 TIMED_WAITING(限时等待)状态的线程不会被分配 CPU 时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下 3 个方法会让线程进入限时等待状态:

      • Thread.sleep(time) 方法,对应的唤醒方式为: sleep 睡眠时间结束。
      • Object.wait(time) 方 法 , 对 应 的 唤 醒 方 式 为 : 调 用 Object.notify() /Object.notifyAll()去主动唤醒,或者限时结束。
      • LockSupport.parkNanos(time)/parkUntil(time) 方法,对应的唤醒方式为:线程调用配套的 LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。

      进入 BLOCKED 状态、 WAITING 状态、 TIMED_WAITING 状态的线程都会让出 CPU 的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入 Ready 状态,需要重新获取时间片才能接着运行。

    • TERMINATED 状态:线程结束任务之后,将会正常进入 TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。

1.5.3,多线程自增 i++ 和线程执行原理

4个线程自增一个堆(共享的)里的对象的值

public class Test {private int i =0;public void f1() {i++;}
}

i++的反编译结果

javap -c Test.class

image-20230801211556153

image-20230801211859089

线程产生时 ,为每个线程的一个虚拟机栈分配1M内存

-Xss128K:指定没有线程的栈大小是128K

自增类:

public class Plus {private int amount = 0;public void selfPlus() {amount ++;}public int getAmount() {return amount;}}

自增任务类

public class PlusTask implements Runnable {private Plus plus;public PlusTask() {}public PlusTask(Plus plus) {this.plus = plus;}@Overridepublic void run() {for (int i = 0; i < 100000000; i++) {plus.selfPlus();}}}

测试

    public static void main(String[] args) throws InterruptedException {Plus plus = new Plus();PlusTask plusTask = new PlusTask(plus);Thread t1 = new Thread(plusTask);Thread t2 = new Thread(plusTask);Thread t3 = new Thread(plusTask);Thread t4 = new Thread(plusTask);t1.start();t2.start();t3.start();t4.start();t1.join();t2.join();t3.join();t4.join();System.out.println("实际的值 = " + plus.getAmount());}

http://www.ppmy.cn/news/997265.html

相关文章

数据结构——单链表OJ题

单链表OJ题 前言一、删除链表中等于给定值 val 的所有节点二、反转一个单链表三、返回链表的中间结点四、输出该链表中倒数第k个结点五、将两个有序链表合并六、链表的回文结构七、将链表分割成两部分八、找出第一个公共结点九、判断链表中是否有环总结 前言 在前面的博客中我…

Stephen Wolfram:机器学习与神经网络训练

Machine Learning, and the Training of Neural Nets 机器学习与神经网络训练 We’ve been talking so far about neural nets that “already know” how to do particular tasks. But what makes neural nets so useful (presumably also in brains) is that not only can t…

网络知识介绍

一、TCP 传输控制协议&#xff0c;Transmission Control Protocol。 面向广域网的通信协议&#xff0c;跨域多个网络通信时&#xff0c;为两个通信端点之间提供一条具有如下特点的通信方式&#xff1a; 基于流、面向连接、可靠通信方式、网络状况不佳时尽量降低系统由于重传带…

线程池的使用详解

一 使用线程池的好处 池化技术相比大家已经屡见不鲜了&#xff0c;线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗&#xff0c;提高对资源的利用率。 线程池提供了一种限制和管理资源&#xff08;包括执行一个任…

【calc】需要使用新应用以打开此ms-calculator链接

问题出现 win10系统&#xff0c;winr输入calc调用计算器时候出现 需要使用新应用以打开此ms-calculator 链接 解决操作 左下角右键单击->Windows Powershell(管理员) # 先执行恢复商店 Get-AppXPackage *WindowsStore* -AllUsers | Foreach {Add-AppxPackage -Disable…

实现一个类,计算程序中创建出了多少个对象

假设命名该类为A&#xff0c;那么A类型的对象一定是经过构造函数或拷贝构造的&#xff0c;那么我们就可以分别定义两个静态成员变量&#xff0c;在构造函数和拷贝构造里变量&#xff0c;这样&#xff0c;每创建一次对象&#xff0c;变量就一次&#xff0c;自然就好求了。如下&a…

【Spring】Spring之依赖注入源码解析

1 Spring注入方式 1.1 手动注入 xml中定义Bean&#xff0c;程序员手动给某个属性赋值。 set方式注入 <bean name"userService" class"com.firechou.service.UserService"><property name"orderService" ref"orderService"…

【Docker】Docker安装Consul

文章目录 1. 什么是Consul2. Docker安装启动Consul 点击跳转&#xff1a;Docker安装MySQL、Redis、RabbitMQ、Elasticsearch、Nacos等常见服务全套&#xff08;质量有保证&#xff0c;内容详情&#xff09; 1. 什么是Consul Consul是HashiCorp公司推出的开源软件&#xff0c;提…