【Spring成神之路】从源码角度深度刨析Spring循环依赖

news2024/12/25 23:42:57

文章目录

  • 一、引言
  • 二、循环依赖出现的场景
    • 2.1 有参构造导致的循环依赖问题
    • 2.2 属性注入出现的依赖问题
    • 2.3 Spring IOC创建Bean的流程
    • 2.4 有参构造为何失败
    • 2.5 属性注入为何能成功
    • 2.6 AOP导致的循环依赖
  • 三、Spring循环依赖源码刨析
  • 四、Spring循环依赖案例刨析

一、引言

循环依赖,顾名思义就是两个Bean互相依赖导致出现的问题,我个人感觉有点死锁的味道,就是A需要实例化的B才能完成实例化,而B而又需要实例化的A才能完成实例化,从而出现了循环依赖问题。

那么Spring是如何解决循环依赖问题的呢?相信这个问题的答案大家随口就能回答,但是我觉得知道理论的回答还不够,我认为还得亲自跟着源码看一遍Spring到底是如何解决的才会印象深刻。

本篇文章会从理论和源码入手,先讲解循环依赖解决的理论部分,后面直接对源码DEBUG,深入理解循环依赖问题。

PS:本篇文章中使用的Spring框架的版本是4.0.0.RELEASE,不同版本之间源码会有一点点不同,但是大体逻辑差不多。

另外,阅读本篇文章之前最好对Spring IOCSpring AOP的实现有一定了解,如果不太了解这两部分可以阅读一下两篇文章:

  1. 【Spring成神之路】一次SpringIOC源码之旅,女朋友也成为了Spring大神!
  2. 【Spring成神之路】老兄,来一杯Spring AOP源码吗?

二、循环依赖出现的场景

循环依赖的出现有两种场景,第一种是有参构造导致的循环依赖问题,第二种是属性注入导致的循环依赖问题,请看下面举的栗子


2.1 有参构造导致的循环依赖问题

先准备两个Bean,两个相互依赖

public class BeanA {
    BeanB beanB;
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
}
public class BeanB {
    BeanA beanA;
    public BeanB(BeanA beanA){
        this.beanA = beanA;
    }
}

配置文件中Bean的注入使用构造器注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="beanA" class="spring.loop.BeanA">
        <constructor-arg ref="beanB"/>
    </bean>

    <bean id="beanB" class="spring.loop.BeanB">
        <constructor-arg ref="beanA"/>
    </bean>

</beans>

测试类

public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("application-loop.xml");
    BeanA beanA = context.getBean(BeanA.class);
    System.out.println(beanA);
}

运行代码

image-20240711223325531

不出所料,Spring框架报错,提示当前请求的Bean真正创建中,是否存在循环依赖


2.2 属性注入出现的依赖问题

改用set方法进行属性注入

public class BeanA {
    BeanB beanB;
    public BeanB getBeanB() {
        return beanB;
    }
    public void setBeanB(BeanB beanB) {
        this.beanB = beanB;
    }
}
public class BeanB {
    BeanA beanA;
    public BeanA getBeanA() {
        return beanA;
    }
    public void setBeanA(BeanA beanA) {
        this.beanA = beanA;
    }
}

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="beanA" class="spring.loop.BeanA">
        <property name="beanB" ref="beanB"/>
    </bean>

    <bean id="beanB" class="spring.loop.BeanB">
        <property name="beanA" ref="beanA"/>
    </bean>

</beans>

测试类

public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("application-loop.xml");
        BeanA beanA = context.getBean(BeanA.class);
        BeanB beanB = context.getBean(BeanB.class);
        System.out.println(beanA);
        System.out.println(beanA.beanB);
        System.out.println(beanB);
        System.out.println(beanB.beanA);
    }
}

image-20240711223850124

运行代码发现,欸?!居然没报错?循环依赖居然成功完成了属性注入!!这到底发生了什么?

别急,看我后面慢慢道来。。。。。


2.3 Spring IOC创建Bean的流程

首先需要先大概了解Spring IOC创建Bean的流程,这里不会涉及源码,如果对源码感兴趣,请看这篇文章【Spring成神之路】一次SpringIOC源码之旅,女朋友也成为了Spring大神!

在Spring框架中,Bean的创建流程如下图所示

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 获取构造方法,并通过构造方法创建实例
  2. 实例化之后进行属性注入
  3. 执行初始化方法,包括前置通知、后置通知等
  4. 最后完成实例化

2.4 有参构造为何失败

image-20240711230046556

由上图可见,调用BeanA的有参构造方法实例化的时候需要BeanB的实例,而实例化BeanB则需要BeanA的实例,两者形成了一个循环依赖,导致无法创建实例。

这个就好比老板需要你把这BUG改完才给你发工资,而你又要老板想给你发工资再改这个BUG,大家都不肯让步。


2.5 属性注入为何能成功

**那属性注入的方式,为何能解决循环依赖呢?**别急,同样的看下图

image-20240711231852822

这里就比较巧妙了,就BeanA来说,创建BeanA的时候,会先调用BeanA的无参构造,实例化一个未完成属性注入的BeanA实例,这时候将这个半成品BeanA放进缓存(earlySingletonObjects,这种缓存也称为提前暴露缓存)中去。

然后BeanA进行属性注入,属性注入需要BeanB的实例,然后开始实例化BeanB,调用BeanB的无参构造方法,实例化了一个未完成属性注入的BeanB,并将其放进缓存中去,BeanB在属性注入阶段需要BeanA的实例,于是会从缓存中获取BeanA实例完成属性注入,接着执行处理初始化方法完成BeanB的实例化。

有了BeanB的实例后,BeanA的创建自然就畅通无阻啦!

这时候就有大聪明就问了,在有参构造的时候我也弄个缓存不行么?

欸?还真不行喔,就上面这个有参构造例子来说,BeanA并不存在半成品之说,要想创建一个BeanA,就必须有一个BeanB的实例才可以,而创建BeanB又需要一个BeanA的实例。

而属性注入这个例子中,BeanABeanB都可以通过无参构造先创建一个实例,无参构造就意味着实例化一个半成品的BeanABeanB的时候不存在任何依赖(当然也不是一定要无参构造,有参构造只要参数不存在循环依赖即可),然后放进缓存供给BeanABeanB完成属性注入即可,这样循环依赖问题自然就迎刃而解啦


2.6 AOP导致的循环依赖

解决Spring的循环依赖问题,需要三级缓存:

  1. 一级缓存(singletonObject:所有创建好的单例Bean
  2. 二级缓存(earlySingletonObjects:完成实例化但是未进行属性注入以及初始化的Bean实例
  3. 三级缓存(singletonFactories:提前暴露的一个单例工厂,二级缓存中存储的就是从这个工厂获取的对象

一级缓存、二级缓存很好理解,三级缓存到底是为了解决什么场景的问题的呢?

试想一下,如果我们创建的对象是一个代理对象,只用一级缓存和二级缓存能够解决循环依赖问题呢?

答案是不会出现循环依赖问题,但是创建出来的对象是不正确的!!!不正确的!!!不正确的!!!

image-20240715204419880

假设BeanA是经过AOP代理的,BeanB则是正常的Bean,BeanA的实例创建好之后会放进缓存池中,接着创建BeanBBeanB的创建完实例之后,进行属性注入,从缓存池中获取BeanA的实例,将缓存池中BeanA的引用赋值给BeanB实例的beanA成员变量

BeanB实例化后,BeanB的实例引用会赋值给BeanA的实例,完成属性注入后,BeanA会进行AOP代理创建出代理对象,并将IOC容器中的beanA的引用指向经过AOP代理后生成的BeanA实例,也就是代理实例。

可见,在没有三级缓存的情况下,BeanB实例中的beanA成员变量的引用是beanA的实例,而不是其代理类的实例,于是就会出现奇奇怪怪的问题。

那怎么解决这个问题呢?

所以三级缓存的存在就是为了解决AOP代理,具体解决的AOP代理的循环依赖的流程如下图所示

image-20240715231658491

区别就在于BeanB的属性注入这里,如果BeanA的实例化是一个AOP代理,则获取其代理对象,否则获取其半实例化对象。

当然,Spring的实现细节和这个图有所区别,但是大体思路差不多


三、Spring循环依赖源码刨析

PS:下面的源码解读会比较乱,就是看起来会感觉联系不起来,但是没关系可以先看一个眼熟,后面看我的流程图还有结合案例就能理解了

直接定位org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean这个方法

protected <T> T doGetBean(
    String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
    throws BeansException {
    String beanName = transformedBeanName(name);
    Object bean;

    // 查询缓存中是否存在这个bean
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null){
        // 省略部分代码
    }else{
        // 缓存中不存在,获取BeanDefinition
        RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
        // 检查是否是单例
        if (mbd.isSingleton()) {
            sharedInstance = getSingleton(beanName, () -> {
                try {
                    return createBean(beanName, mbd, args);
                }
                catch (BeansException ex) {
                    destroySingleton(beanName);
                    throw ex;
                }
            });
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        }
    }
}

org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean方法中,主要有两个流程:

  1. 从缓存中获取Bean
  2. 若缓存没有命中,则创建这个Bean

把目光集中在org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)方法中去

public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 从一级缓存中获取Bean实例
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 一级缓存不存在这个Bean,且当前Bean处于创建中,于是从二级缓存中获取
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            // 二级缓存中不存在这个Bean,获取锁,再从一二三级缓存中尝试获取实例
            synchronized (this.singletonObjects) {
                // Consistent creation of early reference within full singleton lock
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 从三级缓存中回去	
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            // 创建实例
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

这个方法的逻辑很简单,就是从一级、二级、三级缓存中获取Bean实例。在getSingleton返回的不为null,则经过一些处理后就会从doGetBean返回。

如果从缓存中获取不到,会走org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)方法

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
       Object singletonObject = this.singletonObjects.get(beanName);
       if (singletonObject == null) {
           // 一级缓存不存在该Bean
          beforeSingletonCreation(beanName);
          boolean newSingleton = false;
           // 创建单例Bean
           singletonObject = singletonFactory.getObject();
           newSingleton = true;
          // 如果是一个单例则放进一级缓存
          if (newSingleton) {
             addSingleton(beanName, singletonObject);
          }
       }
       return singletonObject;
    }
}

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 将bean放进一级缓存 二级缓存移除该bean
        this.singletonObjects.put(beanName, singletonObject);
        // 三级缓存移除该Bean
        this.singletonFactories.remove(beanName);
        // 二级缓存移除该Bean
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

getSingleton方法的主要作用是从一级缓存中获取Bean,如果Bean不存在则创建缓存

singletonFactory.getObject()实际调用的就是org.springframework.beans.factory.support.AbstractBeanFactory#createBean方法

protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
       throws BeanCreationException {

    RootBeanDefinition mbdToUse = mbd;
    // 创建目标实例
    Object beanInstance = doCreateBean(beanName, mbdToUse, args);
    return beanInstance;

}

先来看创建目标实例的org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean方法吧

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
       throws BeanCreationException {
    // 创建实例
    Object bean = instanceWrapper.getWrappedInstance();
	// 检查是否需要提前暴露这个Bean
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                      isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
		// 如果需要提前暴露,则放进三级缓存中去
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
}

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            // 三级缓存中存储Bean
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

然后再看看org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#getEarlyBeanReference方法

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    // 检查Bean是否被动态代理,如果被动态代理需要返回代理对象
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
       for (BeanPostProcessor bp : getBeanPostProcessors()) {
          if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
             SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
              // 创建代理对象
             exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
          }
       }
    }
    return exposedObject;
}

// 生成代理对象
public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    // 放进二级缓存
    this.earlyProxyReferences.put(cacheKey, bean);
    return wrapIfNecessary(bean, beanName, cacheKey);
}

四、Spring循环依赖案例刨析

PS:这一块需要结合第三部分的源码刨析以及以下流程图进行配合使用,否则会看得稀里糊涂!

就前面BeanABeanB循环依赖的案例来说,下面会一步一步刨析循环依赖的三级缓存是如何操作的。

image-20240715204419880

SpringIOC刷新的时候,会调用org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons进行预处理单例Bean。

假设先初始化BeanA,会调用org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean进行创建Bean。

一开始会先从调用org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)从三级缓存中进行获取BeanA实例,这时候BeanA实例肯定是不存在的。

接着调用org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)方法。

在这个方法中会调用org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])进行创建BeanA实例。

实例创建完成后,会将这个BeanA实例的工厂方法getEarlyBeanReference添加进三级缓存。

getEarlyBeanReference方法是一个获取代理对象的方法,如果前面实例化的BeanA是代理对象,则会将这个对象进行动态代理,返回其代理对象,否则返回前面实例化的BeanA实例。

这时候,三级缓存如下所示

image-20240803150754210

然后BeanA进行属性注入,尝试从SpringIOC中获取BeanB

同理BeanB也会这样被放进第三级缓存

image-20240803153356979

BeanB实例化完成后,会进行属性注入,从缓存中获取BeanA实例

当从缓存中获取BeanA实例的时候,会先执行org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)方法,这时候第三级的缓存始有BeanA的,会拿到BeanA工厂Bean,并调用其getObject方法,也就是前面缓存的getEarlyBeanReference方法获取BeanA的半实例化对象,并放到二级缓存中去。

image-20240803153923386

BeanB获取道BeanA未完成的实例进行完成属性填充后,会调用org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#addSingleton

BeanB在二三级的缓存移除,并将BeanB的实例缓存进一级缓存。

image-20240803155550496

这时候,BeanA的实例化就能获取完成实例化的BeanB进行输入注入,并缓存进一级缓存!

image-20240803155732526

Spring循环依赖的整体流程就是这样子了!


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

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

相关文章

【MATLAB源码】数学建模基础教程---初步认识数学建模

系列文章目录在最后面&#xff0c;各位同仁感兴趣可以看看&#xff01; 什么是数学建模 含义1.区分数学模型和数学建模2. 建立数学模型的注意事项3.数学建模流程图解4.数学建模模型分类5.论文常用套路6.最后&#xff1a;总结系列文章目录 含义 所谓数学建模&#xff0c;简言…

Python 中实现聊天客户端库

在 Python 中实现一个简单的聊天客户端库可以通过使用 socket 模块来处理网络通信。我们可以构建一个基于 TCP 的简单聊天系统&#xff0c;其中包括一个服务器和一个客户端。 1、问题背景 假设您正在尝试编写一个 Python 库&#xff0c;用于实现某个聊天协议的客户端。在连接…

c++入门基础(下篇)————引用、inline、nullptr

引用 引用的概念和定义 引⽤不是新定义⼀个变量&#xff0c;⽽是给已存在变量取了⼀个别名&#xff0c;编译器不会为引⽤变量开辟内存空间&#xff0c; 它和它引⽤的变量共⽤同⼀块内存空间。 类型& 引用别名 引用对象; 就像孙悟空也叫齐天大圣 猪八戒也叫天蓬元帅。…

正点原子imx6ull-mini-Linux驱动之Linux 自带的 LED 灯驱动实验(16)

前面我们都是自己编写 LED 灯驱动&#xff0c;其实像 LED 灯这样非常基础的设备驱动&#xff0c;Linux 内 核已经集成了。Linux 内核的 LED 灯驱动采用 platform 框架&#xff0c;因此我们只需要按照要求在设备 树文件中添加相应的 LED 节点即可&#xff0c;本章我们就来学习如…

Level3答案

突然发现&#xff0c;忘记公布了Level3答案&#xff1a; 1、 (1)heker.h HeiKe.h (2)Make_Text() (3)3 (4)heker.h 2、 (1)ArtText.h Maker_World.h (Maker_Game头文件组) (2)5.0 附加题、 我把标题截了张图&#xff01; 这是我们 Cookie Maker工作室 新出来的 “无标题技术”…

JavaScript基础——JavaScript数据及数据类型

JavaScript中数据的分类 数据是指设备、浏览器可以识别的内容。在JavaScript中&#xff0c;数据可分为基本数据类型&#xff08;值数据类型&#xff09;和引用数据类型。 console.log()函数 浏览器中按下F12或者右击检查&#xff0c;可以打开控制台。 在JavaScript中&#xff0…

微服务通过X-Forwarded-For获取客户端最原始的IP地址

文章目录 引言I 通过转发IP列表获取用户的IP地址II 存储真实IP字段到MDC中2.1 自己存储真实IP字段,方便获取。2.2 feign 传递MDC数据(将MDC中数据传入header)III 处理真实IP(应用)3.1 从MDC获取存储到日志系统中3.2 logback获取MDC数据(IP、追踪码)3.3 打印接口的请求IP引…

教你用python代码写一个中国象棋游戏

编写一个完整的中国象棋游戏是一个复杂的项目&#xff0c;因为它涉及到图形用户界面(GUI)的设计、游戏规则的实现、AI对手的开发等多个方面。不过&#xff0c;我可以提供一个简化的框架和一些基本思路&#xff0c;帮助你开始这个项目。 由于这里不能完整地实现一个图形化的象棋…

三十六、MyBatis-Plus(2)

&#x1f33b;&#x1f33b; 目录 一、CRUD 扩展&#xff08;1)1.1 Insert1.2 主键生成策略1.2.1 源码解释1.2.2 Twitter的snowflake算法 (雪花算法)1.2.3 主键自增&#xff1a;AUTO 我们需要配置主键自增1.2.4 手动输入&#xff1a;INPUT 就需要自己写 id 1.3 Update1.4 自动填…

2024杭电多校第五场

第一题&#xff1a;开关灯 直接暴力找规律。 发现如果n2&#xff08;mod3&#xff09;那么就是2的n-1次方。否则直接是2的n次方。 暴力代码 #include<bits/stdc.h> using namespace std; #define int long longsigned main() {int temp[100];temp[0] 1;for (int i …

SOMEIP_ETS_001:数组长度超过消息长度允许的范围

测试目的&#xff1a; 验证DUT&#xff08;Device Under Test&#xff0c;被测设备&#xff09;在接收到数组长度超过SOME/IP协议允许的最大长度时&#xff0c;是否能够返回错误消息。 描述 本测试用例旨在检查DUT在接收到一个SOME/IP消息时&#xff0c;如果该消息中的数组长…

Java学习:今日成果,明日挑战

阅读指南&#xff1a;[题目] - 精选摘要 题目1.面向对象编程意味着2.以下哪项不是 Java 关键字&#xff1f;3.基础数据类型在堆栈上分配&#xff1f;4.以下代码将导致&#xff1a;5.以下输出是什么 &#xff1f;6.如果我们声明&#xff1a;7.Java 使用按值调用。 以下方法调用传…

S7-1200PLC 和8块欧姆龙温控表MODBUS通信(完整SCL代码)

1、如何提升MODBUS-RTU通信数据的刷新速度 提升MODBUS-RTU通信数据刷新速度的常用方法_modbus rtu通讯慢-CSDN博客文章浏览阅读1.2k次。SMART PLC的MODBUS-RTU通信请参考下面文章链接:【精选】PLC MODBUS通信优化、提高通信效率避免权限冲突(程序+算法描述)-CSDN博客MODBU…

MATLAB预测模型(1)

一、前言 在MATLAB中&#xff0c;解决和预测微分方程通常涉及到使用数值方法&#xff0c;因为许多微分方程的解析解是难以找到的。MATLAB提供了多种函数和工具箱来处理这类问题&#xff0c;其中ode45是最常用的一个&#xff0c;用于求解非刚性微分方程的初值问题。 二、实现 以…

Linux中DHCP服务器配置和管理

文章目录 一、DHCP服务1.1、DHCP的工作流程1.2、DHCP的工作模式1.3、dhcp的主要配置文件 二、安装DHCP服务2.1、更新yum源2.2、安装DHCP服务软件包2.3、配置DHCP服务2.4、启用DHCP服务&#xff08;解决报错&#xff09;2.4.1、查看dhcpd服务的状态和最近的日志条目2.4.2、查看与…

代码随想录27天|贪心

455.分发饼干 代码随想录 第一想法 将孩子胃口值g[i] 按从小到达的顺序排列&#xff0c;饼干尺寸也按照从小到大的顺序去排列。 优先将大尺寸喂给大胃口孩子。如果满足不了胃口那么久试着分给下一个孩子。 要尽量满足更多的孩子&#xff0c;那么大尺寸的饼干就不能喂给小胃口…

PMP–知识卡片--燃起图

燃起图用两条曲线分别绘制随时间的推移、完成的工作量和总工作量的变化情况。它不仅能清晰地展示项目进度&#xff0c;还是对团队成员的一种激励形式。 使用燃起图可以更好地了解进度、范围变更和预期完成时间&#xff0c;它为所有相关方提供了更清晰的进度状态。 燃起图根据工…

抖音豆包大模型AI写作教程

简数采集器支持调用字节跳动抖音的豆包AI大模型API接口&#xff0c;用于对采集的数据进行研究分析&#xff0c;内容写作等。 抖音豆包大模型AI写作使用教程&#xff1a; 目录 1.启用豆包AI大模型API功能 2.设置豆包API处理规则 3.应用API规则处理数据 4.获取AI处理结果 1…

算法 一

时间复杂度 常数操作&#xff1a;和数量无关&#xff0c;每次都是固定时间内完成。 只要高阶项&#xff0c;也不要高阶项的系数。 选择排序、冒泡排序 选择排序&#xff1a;以第一位为起点&#xff0c;每次选择最小的数放在最前面&#xff0c;起点向后挪一位。 冒泡排序&…

冰山的崛起:数据架构的转变

像 Apache Iceberg、Apache Hudi 和 Delta Lake 这样的开放表格式已成为查询处理器的事实标准。然而&#xff0c;最近有消息称 Snowflake 和 Databricks 等查询引擎采用了 Iceberg 的 REST 目录 API&#xff0c;这改变了竞争环境&#xff0c;有利于 Iceberg。 Iceberg的成功不仅…