手写Spring,理解SpringBean生命周期

news2024/11/26 7:39:38

按照Spring使用习惯,准备环境

​ 首先,写一个TigerApplicationContext,模拟Spring的Annotation Application。然后写一个config,接着写一个UserService。

image-20230722183748226image-20230722183803327

由于Spring需要扫描bean,所以我们得定义一个扫描注解ComponentScan,如下:
image-20230722185608545
有了这个扫描注解,我们就可以模拟spring,在config类上写我们需要的扫描路径,如下:
image-20230722185758438

由于我们还需要扫描Service类,所以还需定义一个注解:@Component

image-20230722185949641

通过上面的一些步骤,基本把我们平时该做的一些步骤都设置好了,

例如,通过注解@Component,让Spring扫描,通过在main函数中,获取Spring容器上下文,使用如下语句:

TigerApplicationContext applicationContext = new TigerApplicationContext(AppConfig.class); 

以及通过如下语句,从容器中获取bean.

UserService userService = (UserService)applicationContext.getBean("userService"); 

Spring 扫描字节码文件

​ 但是,我们要知道,这些注解,只是作为给代码看的注释,实际并没有什么真正的作用。像上面的注解:@ComponentScan(“com.tiger.service”),只是告诉Spring需要扫描的路径是:com.tiger.service,至于要怎么扫描,还得通过Spring来实现。下面就说一下,TigerApplicationContext是怎么实现的。

​ 在创建Spring容器的过程中,需要创建非懒加载的单例bean。这个过程是在下面构造方法的过程中实现的:

TigerApplicationContext applicationContext = new TigerApplicationContext(AppConfig.class); 

​ 你让Spring去创建单例bean(通过类的构造函数),那么我得知道哪些是要创建的,那么怎么找到他们?比如说,系统里面有个OrderService类,什么东西也没有加,它就是一个普通的Java类,如下:

image-20230722192446236

​ 所以,Spring得通过扫描注解,才能知道哪些是归Spring容器管理的。那么怎么去扫描呢?

​ 上面的AppConfig类已经很明确的说了,需要在路径com.tiger.service下扫描。扫描的时候,只要加了@Component注解,都需要Spring去创建Bean。

​ 下面看下,在TigerApplicationContext中,是怎么进行扫描的。

image-20230722225533426

好了,通过上面的步骤,我们拿到了扫描路径:com.tiger.service。Spring就是通过解析包路径:com.tiger.service下的类,看看这些类上面,是否有注解。当前我们的项目中,有两个类:OrderService、UserService,由于OrderService没有对应的注解,所以它不是bean。

我们要取的是编译后的class文件。那我们怎么拿到呢?通过class loader去加载Java字节码文件。

image-20230722193754117

java 的类加载系统,分三大类:bootstrap,extend,app。我们程序中的TigerApplicationContext.class 是由App Class loader加载的。

从idea的控制台可以看出,指定的classpath:D:\code\TigerSpring\TigerSpring\target\classes

image-20230722194322951

所以,通过ClassLoader classLoader = TigerApplicationContext.class.getClassLoader(); 来获取对应的类加载器。因为获取到路径是带点号(.)的,但是我们现在是要去文件系统找,所以我们转换一下,把点(.)换成斜杆:path = path.replace(“.”, “/”);

image-20230722195241371

所以通过相对路径,很快就找到了Service文件夹。下面就把resource转换成File 对象,然后再通过File,拿到类的绝对路径。到这里,结合绝对路径,其实可以通过ASM技术操作字节码。但我们本次不使用这个ASM,而是使用Class Loader。

image-20230722201915240

通过Class Loader获取到Class对象,然后再通过class的方法,判断当前的类是否含有@Component注解,如下,打印出了含有注解的类。

image-20230722203238898

而order Service因为没有注解,所以没扫描出来。

image-20230722203632356 image-20230722203712113
那到了这里,可以直接创建bean了吗?还不行,还要判断,他是单例bean,还是原型bean。那怎么判断呢?我们这里新加一个@Scope注解,然后把这个注解添加到类中,如下所示:

image-20230722204229197 image-20230722204256869
接下来就是对@Scope注解写逻辑,通过class判断这个类,有没有使用@Scope注解,主要逻辑如下:

image-20230722204919763

我们先跳过Scope判断逻辑,回到创建bean的地方:

image-20230722205046961

传进来一个字符串,返回一个对象,实现逻辑是什么呢?看一下getBean方法:

image-20230722205257469

​ 大概逻辑可以是这样子:通过字符串bean name,然后扫描文件,再去获取user service的class文件,这个步骤(扫描、加载字节码文件),跟上面扫描注解的步骤,是重复的,由此,我们引入BeanDefinition的概念。

引入BeanDefinition

​ BeanDefinition主要说明一个bean的类型是什么,作用域是什么(单例还是原型),bean是否是懒加载的,具体bean定义如下:

image-20230722205833258

这个bean的定义,是定义所有bean通用的属性。

​ 所以,到这里,我们再回到那个Component注解判断的地方,如果判断了这是一个bean(即使用了@Component注解),那么,就得定义一下BeanDefinition,如下:

image-20230722210604893

由于在扫描方法里面,要扫描整个项目的字节码文件,所用使用的是for循环(有很多的意思),所以我们要把定义好的BeanDefinition放到一个map里面存储,后面我们创建bean的时候,就从这个map里面获取。

image-20230722211518968

小结

​ 传入配置类,获取到配置类上面的扫描路径:com.tiger.service,然后根据扫描路径,去遍历路径下面的每个class文件,通过class loader加载每个class字节码文件,得到一个class对象,得到class对象之后,判断有没有对应的从@Component注解,如果有注解,则定义对应BeanDefinition,然后把BeanDefinition存入到一个map中,这就是扫描包做的事情。

引入单例池

​ 接下来说创建bean。

​ 创建的时候,先做异常判断。由于上面在扫描的时候,我们已经把所有的注解,都扫描出来,放入map里面了。如果bean的名字这个key不在BeanDefinition的keyset中,那么说明这个bean是一个非法的,不存在的bean,所以可以直接把异常抛出来,如下:

image-20230722212457784

​ 如果map里面有这个beanName的key,就接着判断,它是单例的,还是原型的。如果是原型的,直接创建。

为了达到复用的目的,我们不直接在getBean(String beanName)这个方法里面直接创建bean,而是新建一个createBean(String beanName, BeanDefinition beanDefinition)方法。因为单例bean只创建一次,所以在创建的时候,我们可以使用一个map来存储:

private Map<String, Object> singletonObjects = new HashMap<>();

​ 由于我们所有的bean定义在beanDefinitionMap已经有了,我们再次遍历这个map,把所有单例bean拿出来,然后新建,并存入单例map: singletonObjects ,如下:

image-20230722213940211

所以,再回到getBean(String beanName)方法:当判断是单例bean的时候,直接从singletonObjects 中根据bean name 从map取出即可,如果是原型的bean,那么直接调用createBean(String beanName, BeanDefinition beanDefinition) 方法。通过这样子,方法createBean就可以被重用了,拆解成小的函数,便于代码重用。

image-20230722214853851

​ 通过上面,就完整解释了,怎么获取bean。

创建Bean

​ 接下来,说说怎么创建bean,也就是方法:createBean(String beanName, BeanDefinition beanDefinition)

先来个简单版的创建bean:

image-20230722215917700

直接通过class的无参构造函数获取对象。

在main函数运行结果如下:

image-20230722220108299

我们使用orderService再测试一下,给它加上 对应的注解:

image-20230722220435328

在main函数中从容器获取OrderServicebean,并打印,结果如下:

image-20230722220602752

看看OrderService,只有注解,注解里面并没有给bean加名字,和userService有点不一样。

image-20230722220755006 image-20230722220826246

所以,在扫描注解,发现有@Component注解的时候,如果发现没有名字,那就默认给它生成一个名字。

image-20230722221206422

在运行代码,不报错了,如下:

image-20230722221404539

属性注入:引入新的注解@Autowire

​ 我们把orderService注入到userService中,看看打印结果:

image-20230722221953295

打印出来orderService是空值。

下面说说怎么把orderService注入UserService。

image-20230722222152342

按照这个逻辑,通过无参构造函数得到一个对象之后,就开始依赖注入,如下:

image-20230722222411218

​ 注:图怕中有错别字,兑现---->应该是对象

那我要注入什么东西?通过class对象,可以获取到这个类有哪些属性,遍历一下这些属性,看看哪个属性是有@Autowire注解的,如下:

image-20230722222800398

​ 拿到Autowire注解之后,要给这个属性注入什么内容呢?问号这里该给一个什么值?可以根据当前属性的bean类型(也就是UserService这个类的OrderService这个类型)去单例池找,但是,这个时候,orderService在容器中,可能还没有创建好,那此时,只能依赖BeanDefinition。这里先忽略,默认容器中已经有orderService这个bean。但是,如果容器中有多个orderService的bean,那就先通过名字找。找的过程是这样,先根据类型,如果多个,则在根据名字找bean。

​ 综上所述,这里简单点,直接通过名字找bean,调用getBean方法,传入属性名字,如下:

image-20230722223828042

​ 但是,这里可能有一个问题,就是,在调用getBean方法的时候,有可能获取不到。

​ 我们重新梳理一下整个流程。

​ 假设我们在扫描bean的时候,是先创建的UserService这个bean,然后给它的属orderService性赋值,但是这个时候,OrderService并没被扫描到,还没成为Spring IOC容器中的一个bean,那怎么办呢?那上面获取bean的时候,就会返回空,所以我们得加一个判断,如下:

image-20230723095950291

此时,在运行代码,可以看到orderService已经注入到UserService中,如下:

image-20230723100435408

初始化:实现InitializingBean接口

沿着主线继续讲,依赖注入完之后,开始初始化。

image-20230723100605069

我们下面接着模拟InitializingBean接口,接口定义如下:

public interface InitializingBean {
    void afterPropertiesSet();
}

然后让UserService实现接口InitializingBean并重写方法: afterPropertiesSet(),如下:

@Component("userService")
@Scope("prototype")
public class UserService implements InitializingBean {

    @Autowired
    private OrderService orderService;

    public void test(){
        System.out.println("这是user Service 里面的test 方法");
        System.out.println("打印orderService属性:"+ orderService);
    }

    @Override
    public void afterPropertiesSet() {
    }
}

接下来我们把InitializingBean的功能实现一下。这个过程,还是在创建bean的时候实现的,具体代码如下:

判断当前这个bean的实例是否实现了接口InitializingBean,如果实现了,就把bean强制转换为InitializingBean,然后调用afterPropertiesSet方法

            if (instance instanceof InitializingBean){
                ((InitializingBean)instance).afterPropertiesSet();
            }

在任意一个bean(UserService)里面实现InitializingBean接口,Spring就会自动的去调用afterPropertiesSet()方法,这就是初始化的一个简单实现。

初始化前、初始化后:BeanPostProcessor

​ 接下来再讲另外一个重点:BeanPostProcessor,这是我们仿造Spring BeanPostProcessor的一个接口。

public interface BeanPostProcessor {
    default Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean;
    }

    default Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }
}

Spring提供了接口BeanPostProcessor,我们就可以去弄一个它的实现类,如下:

public class TigerBeanPostProcessor implements BeanPostProcessor{
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {

        return bean;
    }
}

类TigerBeanPostProcessor重写了初始化后的方法,那么我们就可以在方法:postProcessAfterInitialization()随意写我们想要实现的逻辑。

这里的逻辑,应该在bean初始化之后调用的。那我们要使用下面的方式调用吗?

image-20230723103946517

这种方式,就是把我们的代码,直接写死到Spring源码里面,肯定不行。

​ 换另外一种方式,使用接口。

​ 大体的思路是这样的:我们不能直接把TigerBeanPostProcessor写死到Spring源码中,但可以通过它的接口:BeanPostProcessor。但是,Spring怎么知道我们有TigerBeanPostProcessor这个类呢?通过注解@Component,如下:

image-20230723104356170

然后回到扫描逻辑,再看方法scan(configClass);

​ 当Spring扫描到有Component注解之后,会判断是否实现了BeanPostProcessor接口,判断方式如下:

image-20230723105018527

如果类实现了接口,那么再通过class获取到类的无参构造函数,从而new一个实例对象出来,这样,通过

BeanPostProcessor instance = (BeanPostProcessor)clazz.getConstructor().newInstance();

就可以替代上面写的硬编码,轻松解耦,不用在Spring源码中new 我们自己业务的实现类。

image-20230723105155193

这个过程是在扫描的方法里面的,但是我们是在创建bean的时候用的,所以,需要把它缓存起来,这个时候,需要新增一个list: beanPostProcessorList接收它。

private List<BeanPostProcessor> beanPostProcessorList = new ArrayList<>();
                            if (BeanPostProcessor.class.isAssignableFrom(clazz)) {
                                BeanPostProcessor instance = (BeanPostProcessor)clazz.getConstructor().newInstance();
                                beanPostProcessorList.add(instance);
                            }

小结:

​ 在扫描的时候,会额外的生成一些beanDefinition,然后存起来,然后呢,这里扫描又额外的做了一些事情,发现如果bean实现了接口BeanPostProcessor,也会把它存起来。

image-20230723105835587

现在通过beanPostProcessorList,我们就可以获取到整个容器中,实现了BeanPostProcessor的实例了。

基于BeanPostProcessor的Spring AOP

​ Spring AOP的底层是基于BeanPostProcessor来实现的。下面来具体说说。

写一个接口:UserInterface

public interface UserInterface {
    public void testAop();
}

然后让UserService实现这个接口,有了接口之后,就可以使用JDK的动态代理了。

image-20230723114253568

回到TigerBeanPostProcessor,写动态代理逻辑,也就是切面逻辑:

@Component
public class TigerBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("BeanPostProcessor相关的");
        Object proxyInstance = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("在调用原始方法之前,执行切面逻辑。。。");
                return method.invoke(bean,args);
            }
        });
        return proxyInstance;
    }
}

使用Proxy的静态方法newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)生成一个代理对象,返回值:proxyInstance就是生成的代理对象。在这里我们可以看到,最后面返回给容器的bean,是代理对象

返回到主函数看看执行效果:

image-20230723115849751

Spring的这个BeanPostProcessor机制非常的牛逼,我们可以基于这个机制,做很多的事情。

例如下面的需求:

​ 我希望在项目中定义一个注解:@TigerValue,使用这个注解后,能把注解里面的value,赋值对加了这个注解的属性。

新增了注解,并运行项目,发现获取到空值:

image-20230723124914424

运行程序:

image-20230723124959805

下面是实现逻辑。增加一个bean初始化前的实现类。

@Component
public class TigerValueBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        for (Field field : bean.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(TigerValue.class)) {
                field.setAccessible(true);
                try {
                    // 把注解里面的值,赋值给变量
                    field.set(bean, field.getAnnotation(TigerValue.class).value());
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        return bean;
    }
}

然后在创建bean之前,调用这个TigerValueBeanPostProcessor实例方法,具体逻辑,跟上面那个初始化后的操作一样,只不过位置放在了bean初始化之前,如下所示:

image-20230723125747223

我们实现BeanPostProcessor接口里面的两个方法,相当于在Spring注册了两个回调函数。当Spring在创建bean的时候,执行到BeanPostProcessor相关的逻辑,就会调用我们实现类里面具体的逻辑。

image-20230723130438884

总结

​ 在main函数中,通过TigerApplicationContext的构造函数,拿到配置类上的扫描路径,然后就去编译好的class文件中扫描,看看哪些类使用了注解。在扫描的过程中,会生成一个BeanDefinition map,把生成的的BeanDefinition 放入Map中,这个map在后面创建bean的时候会用到。此外,在扫描的过程中,还会检查类中是否实现了BeanPostProcessor接口,如果实现了,就把实例化的对象放入到List 中,这个list也是在创建bean的过程中会使用。此外,扫描的时候,还会检查这个是不是单例bean,如果是单例bean,就放入到Map: Map<String, Object> singletonObjects中。

​ 所以,扫描主要做三件事情:生成BeanDefinition ,放入map;把实例化的BeanPostProcessor,放入list;把单例放入Map。

​ 然后开始创建bean。先看下是否有Autowire注解,如果有,从容器中获取bean,注入到属性中然后再看看是否实现了BeanPostProcessor接口,如果实现了,就执行接口的实现方法里面的逻辑,最后生成一个bean实例。这个bean实例,如果有做AOP的话,那么生成的代理对象才是最后返回的bean。

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

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

相关文章

【C++】-priority_queue(优先级队列的具体使用和模拟实现)以及仿函数的简单介绍

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

ubuntu 开启 ssh 服务 设置root远程登录

设置root用户密码 sudo passwd root安装ssh服务和vim编辑器 sudo apt -y install openssh-server vim开启ssh服务 sudo vim /etc/ssh/ssh_config去掉 配置文件中 Port 22 的注释后保存退出 设置root用户远程登录 sudo vim /etc/ssh/sshd_config将 PermitRootLogin prohibit-pas…

文章审核之敏感词过滤

技术选型 DFA实现原理 DFA全称为&#xff1a;Deterministic Finite Automaton,即确定有穷自动机。 存储&#xff1a;一次性的把所有的敏感词存储到了多个map中&#xff0c;就是下图表示这种结构 敏感词&#xff1a;冰毒、大麻、大坏蛋 工具类 最下面的main方法是测试用的&a…

vuejs源码之解析器

解析就是将模版解析成AST。 <div id"app"><p>{{num}}</p> </div>比如下面这个代码&#xff0c;然后转成AST之后是这个样子。 它是用javascript对象来描述一个接待您&#xff0c;一个对象表示一个节点。对象中的属性用来保存节点所需的各种数…

Docker基本概念+命令

Docker基本概念命令 一、Docker是什么&#xff1f;二、为什么Docker技术受欢迎三、Docker核心概念四、Docker安装五、Docker镜像操作1.搜索镜像2.获取镜像3.镜像加速下载4.查看镜像信息5.查看下载的镜像文件信息6.查看下载到本地的所有镜像7.获取镜像的详细信息8.修改镜像标签9…

【复盘与分享】第十一届泰迪杯B题:产品订单的数据分析与需求预测

文章目录 题目第一问第二问2.1 数据预处理2.2 数据集分析2.2.1 训练集2.2.2 预测集 2.3 特征工程2.4 模型建立2.4.1 模型框架和评价指标2.4.2 模型建立2.4.3 误差分析和特征筛选2.4.4 新品模型 2.5 模型融合2.6 预测方法2.7 总结 结尾 距离比赛结束已经过去两个多月了。 整个过…

手机变局2023:一场瞄准产品和技术的“思维革命”

以折叠屏冲高端&#xff0c;已成为中国手机厂商们的共识。 在这个苹果未涉足的领域&#xff0c;国产手机厂商们加快脚步迭代推新&#xff0c;积极抢占机遇。但平心而论&#xff0c;虽然国产折叠屏机型众多&#xff0c;但市场上始终缺乏一款突破性的产品作为标杆&#xff0c;为…

前端监控一vue指令实现埋点

前端监控一vue指令实现埋点 https://v2.vuejs.org/v2/guide/custom-directive.html 自定义指令 需要在main.js中执行 import Vue from vue // 自定义埋点指令 Vue.directive(track, {//钩子函数&#xff0c;只调用一次&#xff0c;指令第一次绑定到元素时调用。在这里可以…

【100天精通python】Day11:面向对象编程_类的定义和使用

目录 1. 面向对象的程序设计概述 2 类的定义和使用 2.1 定义类&#xff1a; 2.2 创建对象实例&#xff1a; 2.3 创建_init_() 方法 2.4 创建类的成员并访问 2.5 访问限制 2.5.1 公开访问&#xff08;Public Access&#xff09;&#xff1a; 2.5.2 私有访问&#xff08;…

深入学习 Redis - 深挖经典数据类型之 set

目录 前言 一、Set 类型 1.1、操作命令 sadd / smembers&#xff08;添加&#xff09; sismember&#xff08;判断存在&#xff09; scard&#xff08;获取元素个数&#xff09; spop&#xff08;删除元素&#xff09; smove&#xff08;移动&#xff09; srem&#x…

剑指27 二叉树的镜像 28.对称的二叉树 26.树的子结构

方法1&#xff1a;队列迭代 方法2&#xff1a;递归 队列迭代&#xff1a; class Solution { public:TreeNode* mirrorTree(TreeNode* root) {queue<TreeNode*> q;if(rootNULL) return root;q.push(root);while(!q.empty()){TreeNode *curq.front();if(!cur) continue;//…

Golang并发控制

开发 go 程序的时候&#xff0c;时常需要使用 goroutine 并发处理任务&#xff0c;有时候这些 goroutine 是相互独立的&#xff0c;需要保证并发的数据安全性&#xff0c;也有的时候&#xff0c;goroutine 之间要进行同步与通信&#xff0c;主 goroutine 需要控制它所属的子gor…

MySQL_2.3【高级查询】超详细讲解

1. distinct关键字 distinct # 在MySQL中&#xff0c;DISTINCT关键字用于消除重复记录&#xff0c;并返回唯一的记录集。 # DISTINCT关键字可以用在SELECT语句的开头&#xff0c;并在查询结果中显示唯一的行。 # 语法如下&#xff1a; select distinct 列1, ... , 列n from t…

vcomp100.dll丢失怎样修复?总结三个简单的修复方法

最近我遇到了一个问题&#xff0c;我的电脑上出现了vcomp100.dll文件丢失的错误。这个错误导致我无法运行一些使用了Microsoft Visual C编写的程序。当我第一次遇到这个问题时&#xff0c;我感到非常困惑和沮丧&#xff0c;因为我不知道如何解决这个问题。 然后&#xff0c;我开…

服务器操作手册——Slurm常用命令

文章目录 引言正文Slurm集群、节点、分区介绍Salloc申请节点并进入查看已经申请的节点终止作业查看集群情况退出节点具体运行问题无法联网问题安装的包找不到 引言 实验室的服务器的操作指令&#xff0c;之前同学写的不够详细&#xff0c;或者说有点乱&#xff0c;这里做一个简…

C++(类与对象)详解 - 1

C&#xff08;类与对象&#xff09;详解 - 1 1.面向过程和面向对象初步认识2.类的引入3.类的定义4.类的访问限定符及封装4.1 访问限定符4.2 封装 5.类的作用域6.类的实例化7.类的对象大小的计算7.1 如何计算类对象的大小7.2 类对象的存储方式7.3 结构体内存对齐规则 8.类成员函…

项目4渗透全过程

网络拓扑图 任务从拓扑图中可以其中一台web服务器可以进行与kali的直接通信。该web服务器是双重网段。也就是拿到该服务器就可以在进行下一步内网操作了。 2008配置&#xff1a; 护卫神主机系统密码&#xff1a;!#Qwe123. sqlserver2008密码&#xff1a;!#a123… 一、信息收…

遥感目标检测(3)-DAL(Dynamic Anchor Learning for Object Detection)

目录 一、概述 二、背景 三、建议 1、旋转RetinaNet 2、动态锚框分布 3、匹配敏感损失 四、实验 一、概述 由于选择正样本锚框进行回归&#xff0c;不一定能够定位真实的GT&#xff0c;而部分负样本回归甚至可以回归到真实的GT&#xff0c;说明相当多的负样本锚框有着准…

FreeRTOS 初识

从这节开始学习FreeRTOS操作系统。 FreeRTOS 介绍 Q: 什么是 FreeRTOS &#xff1f; A: Free即免费的&#xff0c;RTOS的全称是Real time operating system&#xff0c;中文就是实时操作系统。 注意&#xff1a;RTOS不是指某一个确定的系统&#xff0c;而是指一类操作系统。比…

【Java 并发编程】读写锁 ReentrantReadWriteLock StampLock 详解

读写锁 ReentrantReadWriteLock & StampLock 详解 1. 读写锁1.1 并发场景1.2 什么是读写锁1.3 思考如何自己实现一把锁&#xff1f; 2. ReentrantReadWriteLock2.1 ReentrantReadWriteLock 概述及其基本结构2.2 ReentrantReadWriteLock 的特点2.2.1 读写锁的互斥关系2.2.2 …