你真懂循环依赖中缓存的作用?

腾讯课堂某培训机构Spring源码问题集锦

也许从学Spring开始,就开始遇到循环依赖这个名字。在官方文档中也有明确提及。
在这里插入图片描述
从这里可以看出,解决循环依赖可以通过setter注入的方式,何为构造注入、何为setter注入此处不详细解释了。毕竟DI是Spring中必须掌握的重点,参考官方文档即可。

如果说如何解决循环依赖,基本上对Spring有一点深入理解的话,就知道setter注入,而且也能说出个一级缓存、二级缓存、三级缓存的玩意。也知道对应的源码如下所示
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/**
 * Return the (raw) singleton object registered under the given name.
 * <p>Checks already instantiated singletons and also allows for an early
 * reference to a currently created singleton (resolving a circular reference).
 * @param beanName the name of the bean to look for
 * @param allowEarlyReference whether early references should be created or not
 * @return the registered singleton object, or {@code null} if none found
 */
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
	Object singletonObject = this.singletonObjects.get(beanName);
	if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
		synchronized (this.singletonObjects) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
				if (singletonFactory != null) {
					singletonObject = singletonFactory.getObject();
					this.earlySingletonObjects.put(beanName, singletonObject);
					this.singletonFactories.remove(beanName);
				}
			}
		}
	}
	return singletonObject;
}

如果你问为啥要这些缓存?甚至不少人也能回答三级缓存存放的是工厂,二级缓存存放的是半成品,一级缓存存放的是最终对象。

以上的回答没毛病,但是这并不能代表真正懂这里的三级缓存真正的含义。甚至可能还会误解。比如课堂上有同学就疑问:为啥要二级缓存,直接从工厂中获取对象放到一级缓存作为最终对象不就好了吗?

老师回答:主要是因为一个对象可能有多次循环依赖,我大致解释下比如A->B->A,但同时A->C->A,这个时候就需要二级缓存。

后来又有同学问:三级缓存是不是可以不要呀?

老师回答:三级缓存不可以去掉,就算去掉也只能是二级缓存。

…从这段回答,本人对这个老师对循环依赖中缓存理解实在是太失望了。不知道他自己有没有说服自己啊!这个老师对这三级缓存简直一知半解的状态。

我可以负责任的告诉你,这三级缓存一个不能少,而且二级缓存绝对不是解决一个bean存在多个循环依赖的问题的。

好啦!下面开始我们分析循环依赖中三级缓存的作用。

这里首先要弄清楚一个事情,就是我们解决单例的循环依赖问题,单例,对的!单例!这也是为啥要用缓存,这里可不是为了性能问题,而是保证最终Spring容器当中只有这个类型一个对象(所谓容器内单例)。还有第二个点,假如A->B->A,这是循环依赖,但是现在修改了A->B,这两种情况下最终A是不是应该是一样的?按照常理来说,是应该保持一致的,但是你非要不一致,当然也是有可能的。但是,最终放在singletonObjects这一级缓存中的A对象与已经注入到B中的对象是不是一样的?这次我必须回答一定是一样的,因为是单例。总结一下,这里使用这些缓存最终的目标是为了保证单例性,整个容器内甚至被注入的对象都是一个对象。决不能因为缓存导致了不一致和脏读(其他bean引用了一个错误的对象)

只有首先理解了以上的点之后,才可能理解循环依赖中的缓存,要不然都是本末倒置,缘木求鱼。第二个理解上面缓存的关键突破点在于上面getSingleton方法查询二级缓存和三级缓存的条件,除了优先的缓存中没有值(所谓的优先只的是一级缓存优先于二级缓存,二级缓存优先于三级缓存)!还有其他条件,首先二级缓存的可见性取决于isSingletonCurrentlyInCreation,三级缓存的可见性取决于allowEarlyReference。也就是弄懂这三级缓存的根本突破点在于这两个参数的具体用途,也就是Spring官方为啥要这两个参数,可以在Spring的源码中找寻不同取值的场景。

首先对于isSingletonCurrentlyInCreation这个属性,只有在开始创建单例与完成创建单例之间才为true.
在这里插入图片描述
如果还没有开始创建,整个容器当中还没有A的二级缓存和三级缓存,没有必要查询。这一点大多人都知道,但是创建完了呢?为啥这个参数要设置为false,因为创建完了一级缓存中没有(如果用户有定义那么应该是有的,用户没定义那就不可能有了),那就是没有,完全没有必要查询二级缓存和三级缓存了,因为不是调用getSingleton就必须拼命给你找的呀!那么接下来的第二个参数呢?allowEarlyReference使用场景呢?

如果现在从getBean开始来获取一个Bean,会进入到doGetBean -> getSingleton
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
为了保证一个bean的全局唯一(也可以说一致性),那必须先查缓存呀!而此时的allowEarlyReference为true,也就是三级缓存对它是可见的,那么什么时候不可见呢?
在这里插入图片描述
在实例化完成一个Bean之后,注意我说的是实例化,但是还没有依赖注入(populateBean)和初始化(initializeBean),接下来在依赖注入的时候可能会发生循环依赖,比如A->B->A,这样B会在上面标识2的地方注入A,接下来会进入到获取Bean A的过程。也就是上面doGetBean的过程,首先去查询缓存,一级缓存没有、但是A目前正在创建中(isSingletonCurrentlyInCreation),所以二级缓存可见,但是二级缓存也没有,此时allowEarlyReference参数为true,所以对三级缓存也是可见的。接下来三级缓存变为二级缓存。注意三级缓存变为二级缓存不是简单的移除和添加的过程。还包括继续填充的过程,也就是singletonFactory.getObject()的过程。在上图当中的标识1下面的getEarlyBeanReference就是这个过程。

/**
 * Obtain a reference for early access to the specified bean,
 * typically for the purpose of resolving a circular reference.
 * @param beanName the name of the bean (for error handling purposes)
 * @param mbd the merged bean definition for the bean
 * @param bean the raw bean instance
 * @return the object to expose as bean reference
 */
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
	Object exposedObject = bean;
	if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
		for (BeanPostProcessor bp : getBeanPostProcessors()) {
			if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
				SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
				exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
			}
		}
	}
	return exposedObject;
}

因为循环依赖问题 一个刚刚实例化的A -> getEarlyBeanReference -> 二级缓存中(earlySingletonObjects)
但是此时在二级缓存中存在的对象不仅仅是只存在于二级缓存当中,而且存在与B这个单例的引用当中,不妨姑且叫这个对象为earlySingletonObjectOfA。此时一级缓存和三级缓存都不包含单例A的相关引用或其他相关信息。
在这里插入图片描述

接下来B依赖注入其他Bean、初始化、完成构建并将B的单例对象(全容器唯一)存储到一级缓存当中,这个一级缓存中的B和单例A此时指向的都是同一个,不存在问题,保证了单例B的一致性。

由于A还可能依赖其他Bean,所以继续依赖注入和初始化。但是一定要注意,此时继续依赖注入和初始化的那个bean实例,与二级缓存和单例B中的是同一个对象吗?
在这里插入图片描述
getEarlyBeanReference当中经过一系列扩展的后置处理器处理最终成为earlySingletonObjectOfA,而在initializeBean也包含一系列的初始化和前后置处理,很有可能已经不是同一个对象引用了。然后接下来将通过populateBeaninitializeBean,接下来创建完成并存放到一级缓存中。如果是同一个对象还好,不是同一个对象,会有问题吗?当然有问题!
在这里插入图片描述
不得了,出现了脏读,单例B中引用的A并不是容器中的那个单例A,所以,这种情况Spring会让它存在吗?当然不会。那该怎么做?就是创建完成存放到一级缓存之前,会看看初始化initializeBean是否改变了单例的引用。 如果此时二级缓存存在,就会将二级缓存中的对象最终作为单例的最终值,因为其他的实例已经指向了这个二级缓存对象,在这里就是下面的earlySingletonReference对象。
在这里插入图片描述
这里的allowEarlyReference的属性为false,也就是三级缓存不可见,为什么呢?因为只有二级缓存可能被其他bean引用,而不是三级缓存,有人非要说此时三级缓存反正也没有值,何必要不可见呢?那如果在实例化A的过程中根本没有循环依赖发生,此时三级缓存就是存在的,既然没有循环依赖发生,也没必要将三级缓存转为二级缓存了。由此可见,allowEarlyReference的重要性以及三级缓存的重要性。通过这里可以保证一个事情,就是二级缓存(通过getEarlyBeanReference处理)和initializeBean初始化之后必须是同一个对象,保证全局唯一性。如果仔细阅读了getEarlyBeanReference的注释也会加深理解。

/**
 * Obtain a reference for early access to the specified bean,
 * typically for the purpose of resolving a circular reference.
 * <p>This callback gives post-processors a chance to expose a wrapper
 * early - that is, before the target bean instance is fully initialized.
 * The exposed object should be equivalent to the what
 * {@link #postProcessBeforeInitialization} / {@link #postProcessAfterInitialization}
 * would expose otherwise. Note that the object returned by this method will
 * be used as bean reference unless the post-processor returns a different
 * wrapper from said post-process callbacks. In other words: Those post-process
 * callbacks may either eventually expose the same reference or alternatively
 * return the raw bean instance from those subsequent callbacks (if the wrapper
 * for the affected bean has been built for a call to this method already,
 * it will be exposes as final bean reference by default).
 * <p>The default implementation returns the given {@code bean} as-is.
 * @param bean the raw bean instance
 * @param beanName the name of the bean
 * @return the object to expose as bean reference
 * (typically with the passed-in bean instance as default)
 * @throws org.springframework.beans.BeansException in case of errors
 */
default Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
	return bean;
}

在这里插入图片描述
这里为了保证一致,就是上面提到的比较初始化前后的bean是不是被改变了(exposedObject == bean)。
在这里插入图片描述

当然在实现getEarlyBeanReference方法的时候也会考虑这种一致性,但不完全,很多文章说到这里,但是都没有说到根本。比如AOP当中,循环依赖会让一个bean同时涉及到getEarlyBeanReference和initializeBean。在getEarlyBeanReference中会存放代理前的对象,在初始化的时候会比较代理前的对象是不是被改变了,如果没有改变,就不会生成代理了。但是不幸被改变了,就会生成完全不同的代理了。此时问题还是靠上面说的解决最终一致性。
在这里插入图片描述
在这里插入图片描述
那么二级缓存可不可以不要呢?也就是说直接将三级缓存转为一级缓存。

有人回答说,不可以,因为二级缓存中的bean不完整,当然这是一个理由,但有个漏洞,如果是引用同一个对象不完整又有什么区别呢?只要这个引用对象继续被完善,也可以呀!而且如果三级缓存直接转为一级缓存,上面这个比对的过程都没有了,不会发生脏读的问题。那还是没理解二级缓存可见性的问题。因为如果去掉二级缓存,同样就去掉二级缓存可见性的问题,其实三级缓存不但依赖于allowEarlyReference,还隐性的依赖isSingletonCurrentlyInCreation,只有拥有对二级缓存的可见性才能拥有对三级缓存的可见性。如果删除了二级缓存,只使用一个allowEarlyReference来保护三级缓存就非常困难了。而且此时就存放到一级缓存当中,距离这个单例的完成创建还有不少逻辑,怎么保证不会再次被改变呢?也就是说处理脏读会变得非常苦难。而且如果在解决循环依赖时注册一次,然后在创建完成时再注册一次,极有可能冲突,所以Spring中甚至只允许注册一次。
在这里插入图片描述
如上图所示,如果注册多次都会直接报错的。这里的注册机会只能交给最后创建完整的单例Bean。
在这里插入图片描述
在上面获取缓存的时候,获取三级缓存可以升级二级缓存,但是获取二级缓存就不存在升级一级缓存,因为一级缓存只能在这个单例Bean完全创建好之后才可放入的,这样才能完全杜绝对单例A的引用与最后真正的单例A不一致的情况。

所以,最后总结一下:
一级缓存用于存放完整的实例,而且只在整个初始化流程之后才可以,之前都不允许操作。当然了,用户可以手动调用org.springframework.beans.factory.config.SingletonBeanRegistry#registerSingleton接口自己来注册,但是这个bean完全不会参与到Spring的依赖注入、初始化过程了,甚至不存在循环依赖的问题,而且Spring后面也不可能覆盖它,在注册时直接会报错。参考一些这个接口的注释就不难明白了。

/**
 * Register the given existing object as singleton in the bean registry,
 * under the given bean name.
 * <p>The given instance is supposed to be fully initialized; the registry
 * will not perform any initialization callbacks (in particular, it won't
 * call InitializingBean's {@code afterPropertiesSet} method).
 * The given instance will not receive any destruction callbacks
 * (like DisposableBean's {@code destroy} method) either.
 * <p>When running within a full BeanFactory: <b>Register a bean definition
 * instead of an existing instance if your bean is supposed to receive
 * initialization and/or destruction callbacks.</b>
 * <p>Typically invoked during registry configuration, but can also be used
 * for runtime registration of singletons. As a consequence, a registry
 * implementation should synchronize singleton access; it will have to do
 * this anyway if it supports a BeanFactory's lazy initialization of singletons.
 * @param beanName the name of the bean
 * @param singletonObject the existing singleton object
 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
 * @see org.springframework.beans.factory.DisposableBean#destroy
 * @see org.springframework.beans.factory.support.BeanDefinitionRegistry#registerBeanDefinition
 */
void registerSingleton(String beanName, Object singletonObject);

二级缓存只有在出现循环依赖时才会出现,必须存在二级缓存,因为B中注入了A的半成品,这个半成品必须与最后的完成品是一致的,一个对象,在每个单例bean创建完成并注册之前都会跟二级缓存比对是不是同一对象,这样就能有效避免其他bean的脏读问题,所以说二级缓存是为了解决脏读问题。使用二级缓存也能有效减轻多次循环依赖多次调用三级缓存的getObject方法。

三级缓存在单例bean创建过程中是一定会存在的,存在三级缓存就是因为这个缓存不一定会用到,只有在真实发生循环依赖时才会被用到,所以它的可见性只限于循环依赖。其他情况下是不可见的,即便是单例bean初始化之后也是不可见的。

好啦!关于循环依赖中的三级缓存本人的看法就这些了!不知道你怎么看呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值