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

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

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

  1. Bean 开启了懒加载:否则调用 getBeanBean 已经初始化好了;
  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());
    }
}

调试:

通过 IDEADEBUG 功能,调试以上代码,将 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;
}