深入理解SpringBoot(一)----SpringBoot的启动流程分析

news2024/12/24 9:13:39

1、SpringApplication 对象实例化

SpringApplication 文件

public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
      // 传递的source其实就是类Bootstrap
    return new SpringApplication(sources).run(args);
    // 实例化一个SpringApplication对象执行run方法
}

实例化的时候又会执行initialize 方法

private void initialize(Object[] sources) {
      // 这个source依旧是上文说的Bootstrap.class 类
    if (sources != null && sources.length > 0) {
        this.sources.addAll(Arrays.asList(sources));
        // 添加到source资源列表里面去
    }
    this.webEnvironment = deduceWebEnvironment();
    // 设置其是否为web环境
    setInitializers((Collection) getSpringFactoriesInstances(
            ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 拆分为两步,一步是getSpringFactoriesInstances,再者就是set操作
    // set操作很简单,就是设置当前对象的初始化对象以及监听器
    this.mainApplicationClass = deduceMainApplicationClass();
    // 通过堆栈信息,推断 main方法的类对象为当前的主程序类
}

private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",
            "org.springframework.web.context.ConfigurableWebApplicationContext" };

private boolean deduceWebEnvironment() {
    for (String className : WEB_ENVIRONMENT_CLASSES) {
           // 遍历包含上述两个类名称的数组
        if (!ClassUtils.isPresent(className, null)) {
               // 一旦发现不存在该类,就立即返回 deduce 推断不是web环境
            return false;
        }
    }
    // 必须同时包含两个类,才推断出为web环境
    return true;
}

getSpringFactoriesInstances 方法操作

private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
        // 传递的type就是上面说的ApplicationContextInitializer.class以及ApplicationListener.class类
        // 类型以及参数目前都没有具体指
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    // Use names and ensure unique to protect against duplicates
    Set<String> names = new LinkedHashSet<String>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
            // 通过SpringFactoriesLoader 获取对应的名称,具体详情可以看下面的代码块
            // 这点需要重点关注下!!!
            // 结果就是返回一个set集合
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
            // 看样子就是创建一个实例的集合
    AnnotationAwareOrderComparator.sort(instances);
    // 然后通过AnnotationAwareOrderComparator 的排序规则跪实例集合进行排序
    // 排序就是看是否存在Order或者Priority注解,然后取得注解的值,排在集合前面
    return instances;
}

private <T> List<T> createSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
        Set<String> names) {
    List<T> instances = new ArrayList<T>(names.size());
    for (String name : names) {
           // 遍历上面取到的name 集合
        try {
            Class<?> instanceClass = ClassUtils.forName(name, classLoader);
            // 取到这个类名称的类
            Assert.isAssignable(type, instanceClass);
            Constructor<?> constructor = instanceClass
                    .getDeclaredConstructor(parameterTypes);
            // 获取当前类的符合当前参数的构造器
            T instance = (T) BeanUtils.instantiateClass(constructor, args);
            // 利用反射的方式生成具体的对象
            instances.add(instance);
        }
        catch (Throwable ex) {
            throw new IllegalArgumentException(
                    "Cannot instantiate " + type + " : " + name, ex);
        }
    }
    // 最后生成name映射的实例集合
    return instances;
}

SpringFactoriesLoader.loadFactoryNames 方法

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    // 传递的factoryClass 就是上面的ApplicationContextInitializer、ApplicationListener.等
    String factoryClassName = factoryClass.getName();
    // 获取类的全名称
    try {
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        // 如果类加载器为null,则使用系统默认的方法,否则使用当前传递的类加载器读取
        // 当前类加载器可以获取到的所有文件路径为“META-INF/spring.factories” 的地址
        
        List<String> result = new ArrayList<String>();
        while (urls.hasMoreElements()) {
              // 迭代遍历url
            URL url = urls.nextElement();
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            // 读取映射的spring.factories 文件的KV键值对,存放到properties对象中
            String factoryClassNames = properties.getProperty(factoryClassName);
            // 类似于map一般,获取对应的值
            result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
            // 对值使用逗号分隔,生成list,然后去重添加到result
        }
        
        // 总结下来就是遍历当前类环境中的所有路径为“META-INF/spring.factories”的文件
        // 读取文件,然后获取k为当前类名称的所有值,然后存储到set中返回
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
                "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

到这里整个的initialize操作就已经清楚了,通过类加载器可获取的所有为“META-INF/spring.factories” 的地址的文件内容,然后获取key为ApplicationContextInitializer.class以及ApplicationListener.class的类名称的值集合
然后依次就行实例化,最后排序返回,最后保存到当前对象的初始化集合以及监听器集合中,便于后续操作
需要注意到SpringFactoriesLoader.loadFactoryNames 后面很多地方都需要使用该方法去获取相关内容
当然现在只是完成了SpringApplication构造器里面的方法,还剩下后面的run(args)方法执行
如下代码块就是SpringBoot的执行过程(最后的套路依旧是Spring Framework的执行策略)

利用SPI机制扫描 META-INF/spring.factories 这个文件,并且加载 ApplicationContextInitializer、ApplicationListener 接口实例。
1、ApplicationContextInitializer 这个类当springboot上下文Context初始化完成后会调用
2、ApplicationListener 当springboot启动时事件change后都会触发

总结:上面就是SpringApplication初始化的代码,new SpringApplication()没做啥事情 ,利用SPI机制主要加载了META-INF/spring.factories 下面定义的事件监听器接口实现类

2、SpringApplication的run方法启动

public ConfigurableApplicationContext run(String... args) {
    // 这是个计时器
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 记录当前服务开始启动
    ConfigurableApplicationContext context = null;
    // 上下文context,非常关键
    FailureAnalyzers analyzers = null;
    configureHeadlessProperty();
    // 给系统设置headless属性值,就是设置了一些环境变量
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 获取事件监听器SpringApplicationRunListener类型,并且执行starting()方法
    // 就是通过SpringFactoriesLoader 获取到所有SpringApplicationRunListener.class的对象
    // 其中args是用来进行实例化SpringApplicationRunListener对应的对象的构造器参数
    // 最后返回listener是整个系统的监听器
    listeners.starting();
    // 监听器开始执行
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
        // 默认程序参数
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        // 准备运行的环境上下文
        Banner printedBanner = printBanner(environment);
        // 打印banner,默认输出当前springboot版本等内容,可以自定义设置文本或者图片
        // 具体看下面的方法详解
        context = createApplicationContext();
        // 创建SpringBoot最重要的上下文容器
        analyzers = new FailureAnalyzers(context);
        // 分析上下文出现问题的点,便于使用者可以直观的发现问题出现在哪里
        // 其实套路类似,就是使用SpringFactoriesLoader获取所有的FailureAnalyzer实例对象,然后设置其bean工厂为context的bean工厂上下文
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        // 看名称就是对context的前置准备工作,细节在后面说
        refreshContext(context);
        // 切入到spring framework的方式去完成context内容的装载
        // 如果需要注册终止钩子,则注册一个
        afterRefresh(context, applicationArguments);
        // 基本上认为springboot所需的服务都加载完成,进行最后的处理操作
        // 里面常用的就是CommandLineRunner
        listeners.finished(context, null);
        // 监听器的启动结束事件,
        stopWatch.stop();
        // 表示SpringBoot服务启动步骤完成,统计下启动时间等操作
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
              // 打印SpringBoot启动成功的消息,例如 Started xxx in 12.4 seconds 等信息
        }
        return context;
    }
    catch (Throwable ex) {
        handleRunFailure(context, listeners, analyzers, ex);
        // 启动失败了就会输出Application startup failed 日志
        // 并且会输出具体的错误内容信息
        throw new IllegalStateException(ex);
    }
}

private ConfigurableEnvironment prepareEnvironment(
        SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments) {
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 如果当前环境值不为null,直接返回
    // 否则根据上文推断出的webEnvironment boolean 值 生成对象的环境对象
    // 当为true的时候,生成StandardServletEnvironment
    // 否则生成的是StandardEnvironment
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    listeners.environmentPrepared(environment);
    if (!this.webEnvironment) {
          // 如果不是web的环境,再对当前的环境进行包装,生成一个新的运行环境对象
        environment = new EnvironmentConverter(getClassLoader())
                .convertToStandardEnvironmentIfNecessary(environment);
    }
    return environment;
}

private Banner printBanner(ConfigurableEnvironment environment) {
     // 参数environment就是上面生成的环境对象
    if (this.bannerMode == Banner.Mode.OFF) {
          // 如果设置了banner关闭模式,则不进行打印输出操作
        return null;
    }
    ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader
            : new DefaultResourceLoader(getClassLoader());
    // 资源加载器生成
    SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(
            resourceLoader, this.banner);
    // 后续使用SpringApplicationBannerPrinter 类的print进行输出操作
    if (this.bannerMode == Mode.LOG) {
          // 打印模式,如果是log则输出到log中,否则输出到终端中
        return bannerPrinter.print(environment, this.mainApplicationClass, logger);
    }
    return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
    // 大致操作就是先看是否存在自定义的图片类型或者文字类型 banner,如果有就优先确定banner对象
    // 否则就默认使用SpringBootBanner的banner(这个里面就包含了常规的springboot输出内容)
    // 然后解析banner的资源,得出将要输出的字符串内容(利用日志直接输出),存储到PrintedBanner
}

public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework."
        + "boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext";

public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
        + "annotation.AnnotationConfigApplicationContext";

protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            contextClass = Class.forName(this.webEnvironment
                    ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);
            // 如果是web环境,则使用AnnotationConfigEmbeddedWebApplicationContext
            // 否则就使用AnnotationConfigApplicationContext
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                    "Unable create a default ApplicationContext, "
                            + "please specify an ApplicationContextClass",
                    ex);
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);
    // 直接通过类,反射生成无构造参数的对象,一般情况就是AnnotationConfigEmbeddedWebApplicationContext对象了
}

private void prepareContext(ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, Banner printedBanner) {
    // 传递上下文、环境、上下文参数等数据
    context.setEnvironment(environment);
    postProcessApplicationContext(context);
    // 前置处理context上下文,包含了beanNameGenerator和resourceLoader
    // 其中beanNameGenerator 可以自定义规则约定bean的名称功能
    applyInitializers(context);
    // 应用ApplicationContextInitializer去初始化完成对context的操作
    // 具体的ApplicationContextInitializer对象就是在SpringApplication对象的构造方法中实例化创建的
    // 可以给context添加额外的操作,同时也可以很方便的自定义完成自己需要的功能
    listeners.contextPrepared(context);
    // 执行contextPrepared 上下文准备工作的事件
    if (this.logStartupInfo) {
           // 日志启动标志位,默认为true
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
        // 明确当前执行的主函数log,输出SpringBoot的开始启动信息
    }

    // 注册springApplicationArguments 这个bean到context中去
    context.getBeanFactory().registerSingleton("springApplicationArguments",
            applicationArguments);
    if (printedBanner != null) {
        context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
        // 同样是注册,打印早就完成了
    }

    // Load the sources
    Set<Object> sources = getSources();
    // 一般情况下这个source就是SpringBoot 启动的主类Class,注意不是实例对象
    Assert.notEmpty(sources, "Sources must not be empty");
    load(context, sources.toArray(new Object[sources.size()]));
    // 把source也就是主类当做bean,加载到spring的容器中
    listeners.contextLoaded(context);
    // 监听器的上下文导入完成事件 执行
}

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<Object>();
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    AnnotationAwareOrderComparator.sort(runners);
    // 从context获取ApplicationRunner和CommandLineRunner 对象
    // 然后按照对应的规则进行排序
    for (Object runner : new LinkedHashSet<Object>(runners)) {
        if (runner instanceof ApplicationRunner) {
            callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            callRunner((CommandLineRunner) runner, args);
        }
        // 分别执行各自的run方法
    }
    // 一般情况,我们如果需要在SpringBoot加载完成后需要完成一些自定义操作就是注册
    // ApplicationRunner或者CommandLineRunner 的bean对象,然后自定义实现run方法即可
}

3、总结

就SpringBoot的启动整个过程而已,还是很清晰的,SpringBoot的套用SpringFramework的机制,为我们自定义实现功能提供了很好的便利,整个的SpringBoot就是重新包装了一个SpringFramework。
1、new了一个SpringApplication对象,使用SPI技术加载加载 ApplicationContextInitializer、ApplicationListener 接口实例

2、调用SpringApplication.run() 方法

3、调用createApplicationContext()方法创建上下文对象,创建上下文对象同时会注册spring的核心组件类(ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等)。

4、调用refreshContext() 方法启动Spring容器和内置的Servlet容器(tomcat),内置的Servlet容器就是在onRefresh() 方法里面启动的


引用(本文章只供本人学习以及学习的记录,如有侵权,请联系我删除)

SpringBoot 启动过程源码分析
SpringBoot启动流程总结

最后编辑于:2024-09-09 20:07:32


喜欢的朋友记得点赞、收藏、关注哦!!!

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

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

相关文章

CSS—4

1.定位 1.相对定位 2.绝对定位 3.固定定位 4.粘性定位 5.定位的特殊应用 2.布局-版心 3.布局-常用布局名词 4.布局-重置默认样式

【云原生监控】Prometheus监控系统

Prometheus监控系统 文章目录 Prometheus监控系统资源列表基础环境一、部署Prometheus服务1.1、解压1.2、配置systemctl启动1.3、监听端口1.4、访问Prometheus仪表盘 二、部署Node-Exporter2.1、解压2.2、配置systemctl启动2.3、监听端口2.4、访问node-exporter仪表盘 三、配置…

Java接口的艺术:探索接口特性与面向接口编程

在Java的世界里&#xff0c;接口&#xff08;Interface&#xff09;是一种强大的抽象机制&#xff0c;它定义了一组方法规范&#xff0c;但不实现这些方法。接口在Java编程中扮演着至关重要的角色&#xff0c;特别是在实现多态和面向接口编程&#xff08;Interface-Oriented Pr…

Linux进程状态进程优先级

目录 一、操作系统的进程状态 1.1运行状态 1.2阻塞状态 1.3挂起 二、Linux下具体的状态 三、进程的优先级 3.1基本概念 3.2查看进程优先级的命令 3.3修改进程优先级的命令 3.4其他概念 3.5并发 一、操作系统的进程状态 1.1运行状态 当一个进程准备就绪&#xff0c…

MouseArea元素

常用信号 onClicked&#xff0c;鼠标点击onPressed&#xff0c;鼠标按下onReleased&#xff0c;鼠标释放 import QtQuickWindow {width: 640height: 480visible: truetitle: qsTr("Hello World")Rectangle{id:rectwidth: 100height: 100color:"red"MouseA…

redis基本数据结构-sorted set

1. sorted set的简单介绍 参考链接&#xff1a;https://mp.weixin.qq.com/s/srkd73bS2n3mjIADLVg72A Redis的Sorted Set&#xff08;有序集合&#xff09;是一种数据结构&#xff0c;它是一个不重复的字符串集合&#xff0c;每个元素都有一个对应的分数&#xff08;score&…

模板方法模式:设计模式中的骨架法则

模板方法模式&#xff08;Template Method Pattern&#xff09;是一种行为设计模式&#xff0c;它定义了一个操作中的算法骨架&#xff0c;而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 一&#xff0c;模板方法模式的…

C# HttpListener 实现的HTTP Sever浏览器文件下载

1. 前端页面请求 编写简单的test.html 文件&#xff0c;body体值配置a标签&#xff0c;其href 属性设置为文件下载请求的http接口要求的参数序列。 <!DOCTYPE html><html> <head><meta name"viewport" content"widthdevice-width" …

行业分析---自动驾驶行业的发展

1 背景 进入21世纪以来&#xff0c;自动驾驶行业有着飞速的发展&#xff0c;L2级别的自动驾驶技术也逐渐落地量产到寻常百姓家。不管是起步比较早的特斯拉&#xff0c;还是2015年以后国内的公司&#xff0c;都在逐渐发展自动驾驶技术&#xff0c;并量产给用户使用。 自动驾驶最…

COMDEL电源维修CLX2500康戴尔射频电源维修

美国COMDEL射频电源维修常见型号包括&#xff1a;CLX2750&#xff1b;CLX2500&#xff1b;CLX-600H&#xff1b;CX600AS&#xff1b;CX-5000S&#xff1b;CX-3500S&#xff1b;CX-2500S&#xff1b;CV500&#xff1b;CDX2000等。 Comdel成立于1966年&#xff0c;总部设在马萨诸…

Linux环境基础开发工具使用(gcc/g++与makefile)

1.Linux编译器-gcc/g使用 1. 背景知识 接下来的操作&#xff0c;我以gcc为例&#xff0c;因为两者选项都是通用的&#xff0c;所以也就相当于间接学习了 1.预处理&#xff08;进行宏替换) 2.编译&#xff08;生成汇编) 3.汇编&#xff08;生成机器可识别代码&#xff09;…

革新骨科金属螺丝:TPMS结构助力骨再生研究新突破AbMole

在骨科植入物领域&#xff0c;金属螺丝因其出色的机械强度和生物相容性&#xff0c;一直是骨折固定不可或缺的工具。然而&#xff0c;传统实心设计的金属螺丝常常面临应力遮挡和术后松动的问题&#xff0c;严重影响其长期固定效果。近期&#xff0c;一项由吉林大学第二医院骨科…

第T1周:Tensorflow实现mnist手写数字识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 目标&#xff1a; 具体实现&#xff1a; &#xff08;一&#xff09;环境&#xff1a; 语言环境&#xff1a;Python 3.10 编 译 器: PyCharm 框架&#xff1a…

Mixtral 8x7B:开源稀疏混合专家模型的新里程碑

人工智能咨询培训老师叶梓 转载标明出处 随着大模型规模的增大&#xff0c;计算成本和资源消耗也相应增加&#xff0c;这限制了它们的应用范围和效率。本论文介绍了一种新的稀疏混合专家模型&#xff08;SMoE&#xff09;——Mixtral 8x7B&#xff0c;它在保持较小计算成本的同…

【C++】c++ 11

目录 前言 列表初始化 std::initializer_list 右值引用和移动拷贝 左值和右值 左值引用和右值引用的区别 万能引用&#xff08;引用折叠&#xff09; 完美转发 默认成员函数控制 列表初始化 在C98中&#xff0c;标准允许使用花括号{}对数组或者结构体元素进行统一的列…

Gartner 成熟度曲线报告解读(一)| 2024中国IT基础设施使用趋势、影响中国IT使用的4大因素

近些年&#xff0c;面对数字化转型、信息化发展、政策监管与地缘政治等外部因素&#xff0c;以及降本增效的内部需求&#xff0c;不少中国企业在制定 IT 基础设施发展策略时遇到多重挑战。为帮助国内企业用户优化基础设施战略&#xff0c;Gartner 近日发布《中国 IT 基础设施技…

【HCIA-Datacom】华为VRP系统

| &#x1f449;个人主页&#xff1a;Reuuse 希望各位多多支持&#xff01;❀ | &#x1f449;往期博客&#xff1a;网络参考模型 | 最后如果对你们有帮助的话希望有一个大大的赞&#xff01; | ⭐你们的支持是我最大的动力&#xff01;⭐ | 目录 1. 华为VRP系统概述VRP概念设备…

Docker-compose:管理多个容器

Docker-Compose 是 Docker 公司推出的一个开源工具软件&#xff0c;可以管理多个 Docker 容器组成一个应用。用户需要定义一个 YAML 格式的配置文件 docker-compose.yml&#xff0c;写好多个容器之间的调用关系。然后&#xff0c;只要一个命令&#xff0c;就能同时启动/关闭这些…

七、垃圾收集器ParNewCMS与底层三色标记算法详解

文章目录 垃圾收集算法分代收集理论标记-复制算法标记-清除算法标记-整理算法 垃圾收集器1.1 Serial收集器(-XX:UseSerialGC -XX:UseSerialOldGC)1.2 Parallel Scavenge收集器(-XX:UseParallelGC(年轻代),-XX:UseParallelOldGC(老年代))1.3 ParNew收集器(-XX:UseParNewGC)1.4 C…

POSIX信号量以及利用POSIX信号量实现基于循环队列的高效生产者消费者模型

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 &#x1f341;POSIX信号量 &#x1f341;信号量的相关接口介绍*初始化信号量**销毁信号量**等待信号量**发布信号量* &#x1f341;&…