Affects: 6.2.0-M7

After updating to the latest milestone version (6.2.0-M7), an org.springframework.beans.factory.BeanCurrentlyInCreationException is thrown when getting a bean from beanFactory of StaticApplicationContext. Race conditions take place when multiple threads are involved. Apparently, DefaultSingletonBeanRegistry#getSingleton(String beanName, ObjectFactory<?> singletonFactory) is the method where the following scenario takes place:

  1. beforeSingletonCreation(beanName) is called in Thread A,
  2. beforeSingletonCreation(beanName) is called in Thread B,
  3. Because afterSingletonCreation(String beanName) has not yet been called in Thread A, the above step will cause race condition and throwing of BeanCurrentlyInCreationException.

Notes * In version 6.1.11 the exception is not raised. * When using DefaultSingletonBeanRegistry directly instead of through StaticApplicationContext, then no exception is thrown.

Minimal example (might require several runs)

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.support.StaticApplicationContext;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

class BeanFactoryRaceConditionTest {

    private final ExecutorService executorService = Executors.newFixedThreadPool(10);

    @Test
    void testRaceCondition() {
        StaticApplicationContext applicationContext = new StaticApplicationContext();
        applicationContext.registerSingleton("book", Book.class);

        BeanFactory beanFactory = applicationContext.getAutowireCapableBeanFactory();

        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                for (int j = 0; j < 1000; j++) {
                    beanFactory.getBean("book");
                }
            });
        }
        assertThrows(BeanCurrentlyInCreationException.class, () -> {
            for (int i = 0; i < 1000; i++) {
                beanFactory.getBean("book");
            }
        });
    }

    @Test
    void testNoRaceCondition() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        beanFactory.registerSingleton("book", Book.class);

        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                for (int j = 0; j < 1000; j++) {
                    beanFactory.getBean("book");
                }
            });
        }

        assertDoesNotThrow(()->{
            for (int i = 0; i < 1000; i++) {
                beanFactory.getBean("book");
            }
        });
    }

    static class Book {
    }
}

Traces for testRaceCondition

Exception in thread "pool-1-thread-5" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'book': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:424)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:288)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:334)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at com.example.demo.BeanFactoryRaceConditionTest.lambda$testRaceCondition$0(BeanFactoryRaceConditionTest.java:23)

Comment From: liuao1004

Hi, in your code, DefaultSingletonBeanRegistry#registerSingleton means register a bean in the IOC container but not the BeanDefinition. So, when getBean("book"), you will get the Book.class rather than an instance of Book.class which means IOC container does not create singleton bean.

As for the exception, see the source code at line 251 to 274 in DefaultSingletonBeanRegistry, As long as Thread-A acquires the singletonLock and it assigns itself to singletonCreationThread immediately, Thread-B get a not null value from this.singletonCreationThread,then Thread-B will not be blocked. Both of two threads will invoke method beforeSingletonCreation, the latter will throw an exception.

Comment From: jhoeller

As of 6.2 RC1, we are applying the lenient locking fallback to the singleton pre-instantiation phase during a coordinated bootstrap only, exposing concurrent scenarios after bootstrap (e.g. for lazy singletons) to a full singleton lock. This covers the scenario above.