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;
}