Spring 是线程安全的,支持多线程并发调用。但在某种特殊情况下,使用 BeanFactory 的 getBean 方法,我成功拿到了未经初始化的 Bean(是BUG,亦或是出于某种原因考虑的特性?)。

一、获取到的 Bean 为什么未初始化?

通过 getBean 获取到未经过初始化的 Bean 需要满足如下的场景:

  1. Bean 开启了懒加载:否则调用 getBean 前 Bean 已经初始化好了;
  2. 该 Bean 与其他 Bean 产生了循环依赖:未经初始化问题来自于二级缓存 earlySingletonObjects;
  3. 有两个及以上的线程同时调用 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。

注入earlySingletonObjects

然后执行 Thread 线程,将代码阻塞到下图中的位置,可以看到已经成功从 earlySingletonObjects,中获取到了未初始化完成的 Bean。

从earlySingletonObjects取得未初始化化bean

继续步过 Thread 线程,看到控制台未能打印出 twoBean 属性的值,这个属性未完成注入,可见 Bean 还未完成初始化。

Thread线程控制台打印

继续步过 Main 线程,此时由于循环依赖已经处理完成,Bean 已经完成了初始化,所以能成功打印出 twoBean 属性的值。

Main线程控制台打印

三、问题解决

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