Spring
是线程安全的,支持多线程并发调用。但在某种特殊情况下,使用 BeanFactory
的 getBean
方法,我成功拿到了未经初始化的 Bean
(是BUG,亦或是出于某种原因考虑的特性?)。
一、获取到的 Bean 为什么未初始化?
通过 getBean
获取到未经过初始化的 Bean
需要满足如下的场景:
Bean
开启了懒加载:否则调用getBean
前Bean
已经初始化好了;- 该
Bean
与其他Bean
产生了循环依赖:未经初始化问题来自于二级缓存earlySingletonObjects
; - 有两个及以上的线程同时调用
getBean
方法获取该Bean
:并发场景出现的问题。
关键代码片段:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
|+ singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
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;
}
如以上代码,当 A 与 B 两个 Bean
循环依赖,线程一通过 getBean
获取 A,在创建 Bean
过程中因循环依赖执行到第 17 行,将未初始化完成的 Bean
注入到了 earlySingletonObjects
。此时,线程二也通过 getBean
获取 A,在从 singletonObjects
缓存中获取 Bean
失败后,发现 Bean
还在创建中,便从 earlySingletonObjects
获取到了未初始化完全的 Bean
。
二、代码调试
代码:
实体类 OneBean
:
package com.nineya.spring.bean.cycle_thread.entity;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Lazy
@Component
public class OneBean {
@Resource
private TwoBean twoBean;
public OneBean() {
System.out.println("OneBean:实例化");
}
public TwoBean getTwoBean() {
return twoBean;
}
public void setTwoBean(TwoBean twoBean) {
System.out.println("setTwoBean:" + twoBean);
this.twoBean = twoBean;
}
}
实体类 TwoBean
:
package com.nineya.spring.bean.cycle_thread.entity;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Lazy
@Component
public class TwoBean {
@Resource
private OneBean oneBean;
public TwoBean() {
System.out.println("TwoBean:实例化");
}
public OneBean getOneBean() {
return oneBean;
}
public void setOneBean(OneBean oneBean) {
System.out.println("setOneBean:" + oneBean);
this.oneBean = oneBean;
}
}
主方法:
package com.nineya.spring.bean.cycle_thread;
import com.nineya.spring.bean.cycle_thread.entity.OneBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class BeanCycleThreadMain {
public static void main(String[] args) {
System.out.println("## 创建容器");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
System.out.println("## 扫描Bean注解");
context.scan(BeanCycleThreadMain.class.getPackage().getName());
context.refresh();
new Thread(() -> {
System.out.println("## Thread: 通过class取得oneBean");
OneBean oneBean = context.getBean(OneBean.class);
System.out.println("Thread: " + oneBean + " : " + oneBean.hashCode());
System.out.println("Thread: getTwoBean : " + oneBean.getTwoBean());
}).start();
System.out.println("## Main: 通过class取得oneBean");
OneBean oneBean = context.getBean(OneBean.class);
System.out.println("Main: " + oneBean + " : " + oneBean.hashCode());
System.out.println("Main: getTwoBean : " + oneBean.getTwoBean());
}
}
调试:
通过 IDEA
的 DEBUG
功能,调试以上代码,将 Main
线程阻塞到下图中的位置。此时,earlySingletonObjects
中已经注入了一个未初始化完成的 Bean
。
然后执行 Thread
线程,将代码阻塞到下图中的位置,可以看到已经成功从 earlySingletonObjects
,中获取到了未初始化完成的 Bean
。
继续步过 Thread
线程,看到控制台未能打印出 twoBean
属性的值,这个属性未完成注入,可见 Bean
还未完成初始化。
继续步过 Main
线程,此时由于循环依赖已经处理完成,Bean
已经完成了初始化,所以能成功打印出 twoBean
属性的值。
三、问题解决
对 getBean
方法外部包裹 synchronized
关键字,即可解决:
System.out.println("## Thread: 通过class取得oneBean");
synchronized (context.getDefaultListableBeanFactory().getSingletonMutex()) {
OneBean oneBean = context.getBean(OneBean.class);
System.out.println("Thread: " + oneBean + " : " + oneBean.hashCode());
System.out.println("Thread: getTwoBean : " + oneBean.getTwoBean());
}
context.getDefaultListableBeanFactory().getSingletonMutex()
取出的对象实际上就是 singletonObjects
,以上代码的 synchronized
实际上就是对 singletonObjects
加锁。
从上文源码中可见,处理循环依赖属性注入时也会对 singletonObjects
加锁,所以可以避免 Thread
线程在 Main
线程处理属性注入时执行 getBean
方法。
如果是在源码中修改的话,可以修改 DefaultListableBeanFactory
类中的 resolveNamedBean
方法,添加如下逻辑,当对象如果还在创建中的话,就执行获取 singletonObject
锁操作。
private <T> NamedBeanHolder<T> resolveNamedBean(
String beanName, ResolvableType requiredType, @Nullable Object[] args) throws BeansException {
Object bean = getBean(beanName, null, args);
if (bean instanceof NullBean) {
return null;
}
|+ NamedBeanHolder<T> holder = new NamedBeanHolder<>(beanName, adaptBeanInstance(beanName, bean, requiredType.toClass()));
|+ if (isSingletonCurrentlyInCreation(beanName)) {
|+ synchronized (getSingletonMutex()) {
|+ return holder;
|+ }
|+ }
|+ return holder;
}