【Mybatis源码分析】Mybatis查询流程(一级、二级缓存、懒加载原理)

news2024/11/27 2:39:56

Mybatis提供的三种Executor执行器

  • 一、查询流程
  • 二、查询流程总结
  • 三、一级、二级缓存
  • 四、懒加载源码分析

一、查询流程

在讲述 Mybatis 为我们提供的三种 Executor 执行器策略之前,先说说默认情况下 Mybatis 的执行流程。

以下是准备调试的代码:

    <sql id="person_test">
        id,name,age,sex
    </sql>

    <select id="hhh" resultType="com.ncpowernode.mybatis.bean.Person">
        select <include refid="person_test"/>
        from t_person
    </select>
	@Test
    public void testExecutor() throws SQLException {
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatis-config.xml"));
        Configuration configuration = sqlSessionFactory.getConfiguration();

        Environment environment = configuration.getEnvironment();
        TransactionFactory transactionFactory = environment.getTransactionFactory();
        Transaction transaction = transactionFactory.newTransaction(environment.getDataSource(), null, false);

//        Executor executor = new SimpleExecutor(configuration, transaction);
        Executor executor = configuration.newExecutor(transaction);

        List<Object> list = executor.query(configuration.getMappedStatement("com.ncpowernode.mybatis.mapper.PersonMapper.hhh"),
                null, new RowBounds(), null);

        System.out.println(list);

    }

看查询方法时,先知道 sqlsessionFactory 在创建 sqlsession 时,会搞个执行器出来,即下面这个方法(如果你配置了二级缓存,默认是配置的,这用了装饰者模式,这个Executor 是 CachingExecutor 执行器,内部封装了真实执行的委托Executor):

final Executor executor = configuration.newExecutor(tx, execType);

mybatis 默认是会去执行 CachingExecutor 中的 query 方法的,但没有在 mapper 文件中配置 该select语句所在的Mapper,配置了< cache> 或< cached-ref>节点,并且有效
该select语句的参数 useCache=true,所以会走 CachingExecutor 中的委托者中的 query 方法。

执行的查询方法是执行的执行器的父抽象类 BaseExecutor.query 方法,核心代码和相关解释如下:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, 
  RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {

	// 看看是否清除一级缓存
	// 清除一级的缓存的条件是:
	// 1.配置了 flushCacheRequired 参数为true
    // 2. 不存在嵌套查询,就是存在递归
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
    // queryStack 是表示的查询栈个数,递归帧的个数
      queryStack++;
      // 从一级缓存中取,没取到执行 queryFromDatabase 方法,准备从数据库中取
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      // 配置是否使用一级缓存,默认是使用的:SqlSession
      // 如果配置成了 Statement 就表示不适用一级缓存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }
  • 判断这条sql是否是嵌套sql且被标注了清除一级缓存;
  • 从一级缓存中取这条sql的结果集,没找到的话调用 queryFromDatabase 方法获取结果集;
  • 当然一级缓存是和配置的 LocalCacheScope 有关的,即本地缓存设置如果是 SESSION 则表示开启,STATEMENT 则表示不开启。

(有关一级、二级缓存,我再写篇博客详细讲解,现在先分析查询过程)

queryFromDataBase 源码分析
在这里插入图片描述
至于为什么填充个占位符,且这会导致多线程执行同一个查询sql的时候出现ClassCastException异常,为什么的话我在这里阐述了:mybatis查询时一级缓存会引发的问题 。
SimpleExecutor.doQuery 方法源码如下,核心语句查询交给了 RoutingStatementHandler.query 方法。
在这里插入图片描述PreparedStatementHandler.query 方法源码(这里的话也可以知道是先执行sql,然后再处理结果集映,即ORM映射):
在这里插入图片描述处理结果集映射可以看一下,由于代码比较复杂这里截取一些核心的,首先这个resultSetHandler 是 DefaultResultSetHandler 的对象,是通过它去处理结果集映射的。
步骤呢这里梳理一下:

  1. 首先是去获取到对应的结果集 ResultSet,然后将其封装成 ResultSetWrapper 对象;
  2. 然后去获取对应 MappedStatement 的 ResultMap,不管是配置的 resultType 还是 resultMap 都会封装成 ResultMap 让 MappedStatement 封装起来
  3. 然后通过上面俩调用 handleResultSet 方法去处理结果集;
  4. 然后在其方法内部又调用 handleRowValues-handleRowValuesForSimpleResultMap 方法 去处理结果集封装成集合对象,然后放入 multipleResults 中返回。

那我们就去分析 handleRowValuesForSimpleResultMap 方法,它方法内部核心就是去遍历 ResultSet 中的每行数据,将其映射成对象,然后搞成结果集,其核心代码是通过 getRowValue 方法去获取一行结果的结果集对象。在这里插入图片描述getRowValue 就是重中之重啊,即 ORM 的关键,看下面源代码:

  private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // 去拿到结果集对象
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        // 默认是会去执行自动映射的
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      // 根据一堆 ResultMapping 去进行属性映射,这是自己配的,上面是自动的
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix)
        || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
  }

自动映射处理也非常简单,就是拿到那个类型处理器去从 ResultSet 中获取查找的字段值,然后通过 metaObject 工具实体对象去进行映射。

  private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
                                         String columnPrefix) throws SQLException {
    List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
    boolean foundValues = false;
    if (!autoMapping.isEmpty()) {
      for (UnMappedColumnAutoMapping mapping : autoMapping) {
        // 拿到 ResultMapping 中的类型处理器去从 ResultSet 中获取字段值
        final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
        if (value != null) {
          foundValues = true;
        }
        if (value != null || configuration.isCallSettersOnNulls() && !mapping.primitive) {
          // gcode issue #377, call setter on nulls (value is not 'found')
          // 将字段的值给映射到 实体对象中
          metaObject.setValue(mapping.property, value);
        }
      }
    }
    return foundValues;
  }

而根据我们配置的属性进行映射呢,它的关键在与获取字段值,因为在这个 ResultMap 中,除了需要处理简单类型映射外,还要处理嵌套查询,这里的话我们看获取字段值方法,它是核心:getPropertyMappingValue

  private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
                                         ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    // 判断嵌套查询的id是否是空,即select对应的值,是的话表示这个映射不存在嵌套查询
    if (propertyMapping.getNestedQueryId() != null) {
      // 判断是否是嵌套查询映射,是嵌套查询就执行嵌套查询
      return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    }
    if (propertyMapping.getResultSet() != null) {
      addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
      return DEFERRED;
    } else {
    // 这就是简单映射咯
      final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      return typeHandler.getResult(rs, column);
    }
  }

嵌套查询映射呢我们再好好看看源码:

  private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
                                            ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    final String nestedQueryId = propertyMapping.getNestedQueryId();
    final String property = propertyMapping.getProperty();
    // 拿到嵌套查询对应的 MappedStatement 实例
    final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
    final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
    final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping,
      nestedQueryParameterType, columnPrefix);
    
    Object value = null;
    if (nestedQueryParameterObject != null) {
      final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
      final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
        nestedBoundSql);
      final Class<?> targetType = propertyMapping.getJavaType();
      // 判断在一级缓存中是否存在这个嵌套查询
      if (executor.isCached(nestedQuery, key)) {
        // 是一级缓存的话它就直接从一级缓存中获取然后映射了
        executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
        value = DEFERRED;
      } else {
        final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
          nestedQueryParameterObject, targetType, key, nestedBoundSql);
        if (propertyMapping.isLazy()) { // 如果配置了 lazy 的话还会进行懒加载
          // 如果配置了 fetchType 为 lazy 的话
          // 默认是 eager,是不支持懒加载的
          lazyLoader.addLoader(property, metaResultObject, resultLoader);
          value = DEFERRED;
        } else {
        // 可以理解为递归了
          value = resultLoader.loadResult();
        }
      }
    }
    return value;
  }
  • 首先是从一级缓存中拿,一级缓存中存在的话就直接映射了,返回值对象不重要了;
  • 然后在判断是否配置了 fetchType=lazy 即懒加载,配置了就放入懒加载行列中;
  • 没配置懒加载的话就相当于进行了递归操作,去拿到其 executor,然后执行 query 方法。

二、查询流程总结

  • 首先是会创建一个执行器:CachingExecutor;
  • 如果没有配置二级缓存就会自动让代理 Executor 去执行 query 方法。
  • 从一级缓存中获取,没有的话调用 queryFromDatabase 方法去获取结果集,queryDataBase 方法内部会去调用 doQuery 方法;
  • 默认是去预编译获取 PreparedStatement 操纵对象,对参数进行填充后,调用 execute 方法执行 sql
  • 然后调用 handleResultSet 方法去处理结果集映射,它会遍历 ResultSet 中的每行查询结果集,然后将其映射成对象
  • 映射主要有俩处,一处是自动映射,通过 applyAutomaticMappings 方法进行自动映射,一处是根据自己属性配置进行映射,通过 getPropertyMappingValue 方法;
  • 在处理属性配置映射的时候会去处理配置的嵌套查询。
    • 在处理嵌套查询时,是先尝试从一级缓存中获取结果集,如果那个该查询结果集是占位符,说明正在查询,封装到 deferLoad 中。
    • 看看有没有配置懒加载,fetchType=lazy ,配置了的话就会去封装到 lazyLoader 懒加载对象中。
    • 如果上面俩种情况都不存在的话,就直接加载结果,直接进行查询了。

下面是含 Mapper 代理的一个查询过程的简图query

三、一级、二级缓存

个人觉得一级缓存就是去针对嵌套查询的,因为在实际开发中 SqlSession 的生命周期很短,那么一级缓存可以说就是针对 SqlSession 这个会话域中的在内存中的一个缓存,虽然在查询的过程也会去从一级缓存中取,但是最实用的地方还是在属性映射的时候遇到嵌套查询,然后去一级缓存看看有没有存在结果集。

二级缓存的话我觉得是一个全局的缓存,需自己在Mapper中配置cache标签表示使用二级缓存,且要在对应的语句标签上写上 useCache 属性为 true,表示这个语句使用二级缓存,然后会在对应的 MappedStatement 对象中封装这个 Cache 对象。在查询的过程中就会首先走这个二级缓存,不存在的话才会去走代理查询。

下面的图是很好的诠释
在这里插入图片描述
注意:二级缓存需在 sqlSession 提交或者关闭的时候才会把一级缓存刷新到二级缓存才能使用。但在实际开发中由于 sqlSession 的会话很短,所以这个也是不用担心的。(准确点说是执行器内封装了个事务Cache,事务Cache内又封装了咱的二级Cache对象还有个本地缓存,那个本地缓存就本应该是二级内的缓存的,然后提交或者关闭sqlSession之后呢,会调用flushPendingEntries方法将事务Cache里缓存的弄到二级Cache中

当然使用二级缓存可能会存在一些问题,比如分布式下的缓存一致性如何解决;如果有俩个表的查询缓存,会引发缓存不准备的情况等等。

可以看看下面俩篇我觉得优秀文章,图不错~

一级缓存实现机制

二级缓存实现机制

总的来说一级缓存会默认帮你用,嵌套查询显示的明显,二级缓存的话谨慎使用,特别是分布式的情况下。

四、懒加载源码分析

lazyLoader 对象是一个 ResultLoaderMap 对象,其本质是内部封装了一个 hashMap,先用来缓存一下懒加载的对象。
在这里插入图片描述
那它何时被加载呢?肯定是被结果集对象有关,本质是使用了动态代理,只有执行了get方法才会去加载对应的结果集。具体源码我们往下看:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述下面对代理的核心代码进行核心源码解析(每次执行映射对象的方法都会回调这个方法,首先是判断懒加载中是否存在加载对象,然后判断方法不为finalize,它是一个垃圾回收方法):

    @Override
    public Object invoke(Object enhanced, 
    Method method, Method methodProxy, 
    Object[] args) throws Throwable {


      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) {
          if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
          // 这个和配置的 aggressiveLazyLoading 有关,如果配置为true
          // 那么就是说如果遇到 lazyLoadTriggerMethods 配置的相关方法
          // 那么就全部加载lazyLoader中的内容
            if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
              lazyLoader.loadAll();
            } 
           // 如果执行了内部的setter方法就移除对应的懒加载实例
           // 它只判断是不是set方法,是不是修改了属性它不管哦
           else if (PropertyNamer.isSetter(methodName)) {
              final String property = PropertyNamer.methodToProperty(methodName);
              lazyLoader.remove(property);
            } 
					// 如果执行了内部的getter方法就加载结果集
					// 然后通过那个MetaObject对象去映射到真正的结果对象中
					else if (PropertyNamer.isGetter(methodName)) {
              final String property = PropertyNamer.methodToProperty(methodName);
              if (lazyLoader.hasLoader(property)) {
                lazyLoader.load(property);
              }
            }
          }
        }
        return methodProxy.invoke(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
}


注意:aggressiveLazyLoading 在在 3.4.1 及之前的版本中默认为
true,现在新的版本默认是false了。lazyLoadTriggerMethods 的话在配置方法表时候用,分割开。

最后对懒加载的一个总结:

  • 如果你在你写的嵌套查询中配置了懒加载(fetchType=true),那么它在根据 resultType 或者 resultMap 中的 type 属性值去得到映射对象时,就会去搞个动态代理对象,至于这个代理对象是通过 javassist 动态代理工厂实现的
  • 代理方法实现呢就是在执行方法前进行一个回调,如果是 set 方法就移除 lazyLoader 中的加载嵌套查询,如果是 get 就加载对应的嵌套查询,如果配置了 aggressiveLazyLoader ,且方法在配置 lazyLoadTriggerMethods 中,那么就会对 lazyLoader 的所有缓存的进行加载。加载后的结果集会通过 MetaObject 赋值给原始对象。

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

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

相关文章

【LeetCode】19. 删除链表的倒数第 N 个结点

19. 删除链表的倒数第 N 个结点&#xff08;中等&#xff09; 方法&#xff1a;快慢指针 思路 为了找到倒数第 n 个节点&#xff0c;我们应该先找到最后一个节点&#xff0c;然后从它开始往前数 n-1 个节点就是要删除的节点。 对于一般情况&#xff1a;设置 fast 和 slow 两个…

Python0基础自学02-转义字符

Python 都有哪些转义字符&#xff0c;每个案例帮我列举一个代码案例并配上注释 Python中有很多转义字符&#xff0c;它们用于在字符串中插入特殊字符或表示一些特殊的控制序列。以下是一些常用的Python转义字符及其示例代码&#xff1a; 转义字符示例代码print("HelloWo…

学习笔记-BNF、EBNF、ABNF语法格式描述规范

目标是确认一些c/cpp的语法细节&#xff0c;需要看cpp语法定义文件。 考虑从c的语法定义文件开始确认。 考虑实现一个简化的语言定义和编译器&#xff0c;为后续的实际需求做自定义扩展。 参考网页&#xff1a; https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_f…

Linux内核源码分析 (B.2)虚拟地址空间布局架构

Linux内核源码分析 (B.2)虚拟地址空间布局架构 文章目录 Linux内核源码分析 (B.2)虚拟地址空间布局架构一、Linux内核整体架构及子系统二、Linux内核内存管理架构 一、Linux内核整体架构及子系统 Linux内核只是操作系统当中的一部分&#xff0c;对下管理系统所有硬件设备&…

R语言STAN贝叶斯线性回归模型分析气候变化影响北半球海冰范围和可视化检查模型收敛性...

原文链接&#xff1a;http://tecdat.cn/?p24334 像任何统计建模一样&#xff0c;贝叶斯建模可能需要为你的研究问题设计合适的模型&#xff0c;然后开发该模型&#xff0c;使其符合你的数据假设并运行&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 相关视频…

Flask狼书笔记 | 06_电子邮件

文章目录 6 电子邮件6.1 使用Flask-Mail发送6.2 使用事务邮件服务SendGrid6.3 电子邮件进阶6.4 小结 6 电子邮件 Web中&#xff0c;我们常在用户注册账户时发送确认邮件&#xff0c;或是推送信息。邮件必要的字段包含发信方(sender)&#xff0c;收信方(to)&#xff0c;邮件主题…

Miniconda3环境迁移

问题&#xff1a; conda之前安装的默认路径空间满了没法进行安装&#xff0c;为此将其进行迁移&#xff0c;但是迁移之后报错 bash: /data/anaconda3/bin/conda: /home/anaconda3/bin/python: 坏的解释器: 没有那个文件或目录解决方案&#xff1a; 1、修改~/.bashrc中的环境…

用通俗易懂的方式讲解大模型分布式训练并行技术:数据并行

近年来&#xff0c;随着Transformer、MOE架构的提出&#xff0c;使得深度学习模型轻松突破上万亿规模参数&#xff0c;传统的单机单卡模式已经无法满足超大模型进行训练的要求。因此&#xff0c;我们需要基于单机多卡、甚至是多机多卡进行分布式大模型的训练。 而利用AI集群&a…

【多尺度双域引导网络:Pan-sharpening】

Multi-Scale Dual-Domain Guidance Network for Pan-sharpening &#xff08;用于泛锐化的多尺度双域引导网络&#xff09; 全色锐化的目标是在纹理丰富的全色图像的指导下&#xff0c;通过超分辨低空间分辨率多光谱图像&#xff08;LRMS&#xff09;的对应物产生高空间分辨率…

Dedecms最新版--0day分享分析(二)

前言 接上一篇的Tricks&#xff0c;既然利用远程文件下载方式成为了实现RCE的最好方法&#xff0c;毕竟在执行的时候没有恶意shell文件&#xff0c;恶意木马被存放于远端服务器&#xff0c;那么下文的day就是对远程恶意文件的利用。 环境 下载最新版本&#xff1a; https://…

前端项目启动时报错:Use // eslint-disable-next-line to ignore the next line

前端项目启动时报错&#xff1a;Use // eslint-disable-next-line to ignore the next line 首先说一下这个问题产生的原因&#xff1a; 项目创建时设置了使用 eslint 进行代码规范检查。 解决办法&#xff1a; 找到webpack.base.conf.js文件&#xff0c;并且将下满这行代码…

在 linux 虚拟机上安装配置 hive

目录 一 下载hive 安装包 二 解压 hive 并配置环境变量 三 配置hive 的配置文件 四 更新 guava 五 hive初始化 六 开启远程连接 七 使用datagrip 连接 hive 一 下载hive 安装包 百度网盘资源如下&#xff1a; 链接: https://pan.baidu.com/s/18jF-Qri0hc52_rtL61O0YQ?…

常见的CSS兼容问题和解决方案

今天就来聊聊在为了您更好的体验&#xff0c;本文章聊聊如何仅支持谷歌浏览器访问查看页 前端开发经常需要检查浏览器的兼容性&#xff0c;这里推荐(Can I Use)这个查询网站。它是一个针对前端开发人员定制的一个查询CSS、JS、HTML5、SVG在主流浏览器中特性和兼容性的网站&…

RabbtiMQ的安装与使用

一、安装Erlang与Rabbitmq 安装教程本教程是在centos8下试验的&#xff0c;其实linux系统的都差不多RabbitMQ官方&#xff1a;Messaging that just works — RabbitMQRabbitMQ是开源AMQP实现&#xff0c;服务器端用Erlang语言编写&#xff0c;Python、Ruby、 NET、Java、JMS、c…

axios返回几种数据格式? 其中Blob返回时的size是什么意思?

axios返回几种数据格式? 其中Blob返回时的size是什么意思&#xff1f; 1、字符串&#xff08;String&#xff09;&#xff1a;服务器可以返回纯文本或HTML内容&#xff0c;Axios会将其作为字符串返回。 2、JSON&#xff08;JavaScript Object Notation&#xff09;&#xff…

C语言入门 Day_14 for循环

前言 我们定义了一个数组以后&#xff0c;要使用&#xff08;读取或者修改&#xff09;数组元素的话&#xff0c;可以一个一个的读取&#xff0c;就前两课学的那样&#xff0c;代码类似这个结构。 int number_list[5]{1,2,3,4,5}; printf("%d\n",number_list[0]); …

【android12-linux-5.1】【ST芯片】【RK3588】【LSM6DSR】HAL移植

一、环境介绍 RK3588主板搭载Android12操作系统,内核是Linux5.10,使用ST的六轴传感器LSM6DSR芯片。 二、芯片介绍 LSM6DSR是一款加速度和角速度(陀螺仪)六轴传感器,还内置了一个温度传感器。该芯片可以选择I2C,SPI通讯,还有可编程终端,可以后置摄像头等设备,功能是很…

第二章 Linux多进程开发 2.24-2.31 信号集及相关函数 共享内存 守护进程

有时间需要重新回顾 2.24 信号集及相关函数 1.用户通过键盘 Ctrl C, 产生2号信号SIGINT (信号被创建) 2.信号产生但是没有被处理 &#xff08;未决&#xff09; - 在内核中将所有的没有被处理的信号存储在一个集合中 &#xff08;未决信号集&#xff09; - SIGINT信号状态被…

第 3 章 栈和队列 (利用非循环顺序队列采用广度搜索法求解迷宫问题(一条路径))

1. 背景说明 广度优先通俗的解释的是将当前所有能走的步骤全部走完并保存在队列中&#xff0c;又称为层序遍历&#xff0c;此外&#xff0c;该方法类似于多条路线并发前进&#xff0c; 哪一条先到就取哪条路线作为结果并终止查询&#xff0c;因此能够得到最短路径&#xff0c;…

【JavaEE】_HTML

目录 1.HTML结构 2. HTML常用标签 2.1 注释标签 2.2 标题标签&#xff1a;h1~h6 2.3 段落标签&#xff1a;p 2.4 换行标签&#xff1a;br 2.5 格式化标签 2.6 图片标签&#xff1a;img 2.7 超链接标签&#xff1a;a 2.8 表格标签 2.9 列表标签 2.10 表单标签 2.10…