2024年Java最全手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘,附小技巧

最后希望可以帮助到大家!

千千万万要记得:多刷题!!多刷题!!

之前算法是我的硬伤,后面硬啃了好长一段时间才补回来,算法才是程序员的灵魂!!!!

篇幅有限,以下只能截图分享部分的资源!!

(1)多线程(这里以多线程为代表,其实整理了一本JAVA核心架构笔记集)

image

(2)刷的算法题(还有左神的算法笔记)

image

(3)面经+真题解析+对应的相关笔记(很全面)

image

(4)视频学习(部分)

ps:当你觉得学不进或者累了的时候,视频是个不错的选择

在这里,最后只一句话:祝大家offer拿到手软!!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

//成品缓存

private static final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

//半成品缓存

private static final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();

//从缓存中获取

private static Object getSingleton(String className) {

//先从成品缓存中查找

Object singletonObject = singletonObjects.get(className);

if (singletonObject == null) {

//再从半成品缓存中查找

singletonObject = earlySingletonObjects.get(className);

}

return singletonObject;

}

@SuppressWarnings(“unchecked”)

private static T getBean(Class clazz) throws IllegalAccessException, InstantiationException {

//先从缓存中获取

String className = clazz.getSimpleName();

Object singleton = getSingleton(className);

if (singleton != null) {

return (T) singleton;

}

synchronized (singletonObjects) {

singleton = singletonObjects.get(className);

//这里需要再进行一次检查

if (singleton != null) {

return (T) singleton;

}

//实例化对象

T instance = clazz.newInstance();

//实例化完成后,就将这个半成品放入到缓存中

earlySingletonObjects.put(className, instance);

//获取当前类中的所有字段

Field[] fields = clazz.getDeclaredFields();

for (Field field : fields) {

//允许访问私有变量

field.setAccessible(true);

//判断字段是否被@Load注解修饰

boolean isUseLoad = field.isAnnotationPresent(Load.class);

if (!isUseLoad) {

continue;

}

//获取需要被注入的字段的class

Class<?> fieldType = field.getType();

//递归获取字段的实例对象

Object fieldBean = getBean(fieldType);

//将实例对象注入到该字段中

field.set(instance, fieldBean);

}

//完成属性注入后,从半成品缓存中移除,加入到成品缓存中

earlySingletonObjects.remove(className);

singletonObjects.put(className, instance);

return instance;

}

}

public static void main(String[] args) {

new Thread(() -> {

try {

A a1 = getBean(A.class);

System.out.println(“t1.a:” + a1.hashCode());

System.out.println(“t1.b:” + a1.getB().hashCode());

} catch (IllegalAccessException | InstantiationException e) {

e.printStackTrace();

}

}).start();

new Thread(() -> {

try {

A a1 = getBean(A.class);

System.out.println(“t2.a:” + a1.hashCode());

System.out.println(“t2.b:” + a1.getB().hashCode());

} catch (IllegalAccessException | InstantiationException e) {

e.printStackTrace();

}

}).start();

new Thread(() -> {

try {

B b = getBean(B.class);

System.out.println(“t3.b:” + b.hashCode());

System.out.println(“t3.a:” + b.getA().hashCode());

} catch (IllegalAccessException | InstantiationException e) {

e.printStackTrace();

}

}).start();

}

输出结果:

手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

多次实验,从输出结果说明:在多线程的场景下,使用两级缓存能够有效避免出现空指针的问题,在一定程度上也能比整个方法加锁的效率更高。

手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

五、什么样的循环依赖都能解决吗?

================

从第二节可以看出,多例模式下就不可以解决循环依赖。

我们在以上小节中写的代码,是默认全部使用反射set注入的。

而对于单例模式,在经过对依赖项自然排序后,构造器注入是不可以优先于任何一个set注入的。

手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

第一种场景:b依赖a,需要使用构造器注入a;a依赖b,需要使用set注入b

经过Spring对Bean的自然排序后,会先去创建a,再去创建b。

手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

这种场景,是可以解决循环依赖的。在实例化B时,已经存在半成品a。

结论:最后再使用构造器注入时,可以解决循环依赖。

第二种场景:a依赖b,需要使用构造器注入b;b依赖a,需要使用set注入a

手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

这种场景,在实例化a时,就需要调用构造方法,因此去实例b,而b在缓存中找不到a,造成注入失败。

结论:一开始就使用构造器注入,则不能解决循环依赖。

那么都使用构造器注入时,那肯定也不能解决循环依赖的。

因此,Spring解决循环依赖有两个小前提:

  • 循环依赖中的Bean都是单例

  • 在经过自然排序后,构造器注入不能优先于循环依赖中的任何一个set注入

六、Spring中解决循环依赖的原理

==================

在Spring中,我们使用getBean方法从容器获取一个Bean,那么就从getBean方法入手

AbstractApplicationContext类中的getBean

public Object getBean(String name) throws BeansException {

assertBeanFactoryActive();

return getBeanFactory().getBean(name);

}

接着进入AbstractBeanFactory类中的getBean

public Object getBean(String name) throws BeansException {

return doGetBean(name, null, null, false);

}

再进入到doGetBean方法中,该方法比较长,截取其中比较核心的点来说

先说getSingleton方法

//从缓存中获取指定的bean

Object sharedInstance = getSingleton(beanName);

进入到

DefaultSingletonBeanRegistry的getSingleton方法中

public Object getSingleton(String beanName) {

return getSingleton(beanName, true);

}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {

//先从一级缓存中查找

Object singletonObject = this.singletonObjects.get(beanName);

//如果一级缓存中没有,且当前bean正处于创建的过程中

if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {

synchronized (this.singletonObjects) {

//从二级缓存中查找

singletonObject = this.earlySingletonObjects.get(beanName);

//如果二级缓存中也没有,且允许暴露早期引用时

if (singletonObject == null && allowEarlyReference) {

//从三级缓存中查找到bean的工厂

ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);

if (singletonFactory != null) {

//调用getObject方法生成bean

singletonObject = singletonFactory.getObject();

//放入到二级缓存中

this.earlySingletonObjects.put(beanName, singletonObject);

//从三级缓存中移除

this.singletonFactories.remove(beanName);

}

}

}

}

return singletonObject;

}

看到这里,似乎和之前我们写的代码很像啊。

Spring在解决循环依赖时,其实也用到了缓存,缓存声明及定义如下:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

singletonObjects,一级缓存,存放的是最终的成品,即完成实例化(仅调用构造方法)、属性注入、初始化后的bean

earlySingletonObjects,二级缓存,存放的是半成品,或叫早期曝光对象,即只完成实例化,未完成属性注入及初始化的bean

singletonFactories,三级缓存,存放的是能获取到半成品的工厂

前几节我们自己解决了循环依赖,一级缓存的存在是为了在单线程的情况下解决循环依赖,而二级缓存的存在是为了兼容多线程,提升获取bean的效率。那三级缓存存在的意义又是什么呢?

手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

解决循环依赖,其中的一个核心前提是bean必须是单例的,因此我们接着看doGetBean方法中第二处核心的代码,即处理单例bean的代码

/处理单例bean

if (mbd.isSingleton()) {

//sharedInstance就是从缓存中获取的bean,一般来说,这里是null的

sharedInstance = getSingleton(beanName, () -> {

try {

return createBean(beanName, mbd, args);

} catch (BeansException ex) {

destroySingleton(beanName);

throw ex;

}

});

bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);

}

其中getSingleton方法中的第二个参数是一个函数式接口类型的ObjectFactory,因此这里可以直接使用lambda表达式来传入一个默认的实现,即createBean方法。

进入到getSingleton(beanName,objectFactory)方法中,精简后的代码如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {

//如果不在缓存中,就会利用getObject方法去创建

synchronized (this.singletonObjects) {

Object singletonObject = this.singletonObjects.get(beanName);

if (singletonObject == null) {

//标志当前bean正在创建中,如果被多次创建,这里也会抛出异常

beforeSingletonCreation(beanName);

boolean newSingleton = false;

//执行getObject,即执行外部传入的createBean方法

singletonObject = singletonFactory.getObject();

newSingleton = true;

//省略异常处理,出现异常时,newSingleton=false

//取消bean正在创建的标志

afterSingletonCreation(beanName);

if (newSingleton) {

//管理缓存,移除三级缓存与二级缓存,加入到一级缓存中

addSingleton(beanName, singletonObject);

}

}

return singletonObject;

}

}

当走到getObject方法时,就会进入到

AbstractAutowireCapableBeanFactorycreateBean方法中核心的方法是这一句话

Object beanInstance = doCreateBean(beanName, mbdToUse, args);

接着进入到同类中的doCreateBean方法中,精简之后的代码:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)

throws BeanCreationException {

// 实例化bean

BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);

boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&

isSingletonCurrentlyInCreation(beanName));

if (earlySingletonExposure) {

//加入到三级缓存中,getEarlyBeanReference会返回单例工厂

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

}

Object exposedObject = bean;

//属性注入

populateBean(beanName, mbd, instanceWrapper);

//初始化

exposedObject = initializeBean(beanName, exposedObject, mbd);

if (earlySingletonExposure) {

//从二级缓存中查找

Object earlySingletonReference = getSingleton(beanName, false);

if (earlySingletonReference != null) {

//返回二级缓存中的bean,这里就有可能是代理后的对象

exposedObject = earlySingletonReference;

}

}

return exposedObject;

}

看看getEarlyBeanReference到底返回了什么样的一个单例工厂(但其实说是工厂,不如说是获取早期曝光对象的一个逻辑)

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {

Object exposedObject = bean;

//从容器中寻找实现InstantiationAwareBeanPostProcessor接口的后置处理器

if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {

//遍历找到的所有符合要求的后置处理器

for (BeanPostProcessor bp : getBeanPostProcessors()) {

//如果后置处理器实现了SmartInstantiationAwareBeanPostProcessor接口

if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {

SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;

//调用SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法

exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);

}

}

}

return exposedObject;

}

在getEarlyBeanReference方法中,最终会返回一个经过AOP拦截后生成的代理对象。

如果当前没有任何实现

InstantiationAwareBeanPostProcessor接口的后置处理器,即当前bean没有被任何AOP拦截后,那直接返回传进来的bean。

到这里,源码已经分析得差不多了。以A与B的循环依赖,画一下整个过程

以A没被代理为例:

手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

A被代理为例:

手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

从上面的分析可以看出,解决循环依赖的本质是借助于缓存。

多例下的循环依赖,每次注入的都是一个新的对象,完全用不到缓存,所以无法解决多例下的循环依赖。

在经过自然排序后,如果一开始就是构造器注入,也无法利用到缓存。set注入之所以能利用到缓存,是因为能够将实例化与属性赋值分离,给缓存利用留下余地。

对三级缓存的理解

========

在单线程的情况,也就是getBean仅支持串行操作的话,那么一级缓存其实已经够用了。

二级缓存将半成品与成品对象分离,使得多线程的情况下,不会拿到不完整的对象实例。而且支持多线程同时查询缓存,在一定程度上提升了性能。

如何快速更新自己的技术积累?

  • 在现有的项目里,深挖技术,比如用到netty可以把相关底层代码和要点都看起来。
  • 如果不知道目前的努力方向,就看自己的领导或公司里技术强的人在学什么。
  • 知道努力方向后不知道该怎么学,就到处去找相关资料然后练习。
  • 学习以后不知道有没有学成,则可以通过面试去检验。

我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目

八年CRUD,疫情备战三个月,三面头条、四面阿里拿offer面经分享

八年CRUD,疫情备战三个月,三面头条、四面阿里拿offer面经分享

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

,也无法利用到缓存。set注入之所以能利用到缓存,是因为能够将实例化与属性赋值分离,给缓存利用留下余地。

对三级缓存的理解

========

在单线程的情况,也就是getBean仅支持串行操作的话,那么一级缓存其实已经够用了。

二级缓存将半成品与成品对象分离,使得多线程的情况下,不会拿到不完整的对象实例。而且支持多线程同时查询缓存,在一定程度上提升了性能。

如何快速更新自己的技术积累?

  • 在现有的项目里,深挖技术,比如用到netty可以把相关底层代码和要点都看起来。
  • 如果不知道目前的努力方向,就看自己的领导或公司里技术强的人在学什么。
  • 知道努力方向后不知道该怎么学,就到处去找相关资料然后练习。
  • 学习以后不知道有没有学成,则可以通过面试去检验。

我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目

[外链图片转存中…(img-CPmVAdkj-1714889851145)]

[外链图片转存中…(img-yyvlVrnH-1714889851146)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 11
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值