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:
beforeSingletonCreation(beanName)
is called in Thread A,beforeSingletonCreation(beanName)
is called in Thread B,- Because
afterSingletonCreation(String beanName)
has not yet been called in Thread A, the above step will cause race condition and throwing ofBeanCurrentlyInCreationException
.
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.