Skip to content

Commit b336bbe

Browse files
committed
Wait for lenient bean creation in locked thread when necessary
Closes spring-projectsgh-34349
1 parent 056757b commit b336bbe

File tree

2 files changed

+102
-7
lines changed

2 files changed

+102
-7
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java

+47-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Set;
2626
import java.util.concurrent.ConcurrentHashMap;
27+
import java.util.concurrent.locks.Condition;
2728
import java.util.concurrent.locks.Lock;
2829
import java.util.concurrent.locks.ReentrantLock;
2930
import java.util.function.Consumer;
@@ -100,6 +101,15 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
100101
/** Names of beans currently excluded from in creation checks. */
101102
private final Set<String> inCreationCheckExclusions = ConcurrentHashMap.newKeySet(16);
102103

104+
/** Specific lock for lenient creation tracking. */
105+
private final Lock lenientCreationLock = new ReentrantLock();
106+
107+
/** Specific lock condition for lenient creation tracking. */
108+
private final Condition lenientCreationFinished = this.lenientCreationLock.newCondition();
109+
110+
/** Names of beans that are currently in lenient creation. */
111+
private final Set<String> singletonsInLenientCreation = new HashSet<>();
112+
103113
/** Flag that indicates whether we're currently within destroySingletons. */
104114
private volatile boolean singletonsCurrentlyInDestruction = false;
105115

@@ -243,6 +253,7 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
243253
Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
244254
boolean acquireLock = !Boolean.FALSE.equals(lockFlag);
245255
boolean locked = (acquireLock && this.singletonLock.tryLock());
256+
boolean lenient = false;
246257
try {
247258
Object singletonObject = this.singletonObjects.get(beanName);
248259
if (singletonObject == null) {
@@ -257,6 +268,14 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
257268
Thread.currentThread().getName() + "\" while other thread holds " +
258269
"singleton lock for other beans " + this.singletonsCurrentlyInCreation);
259270
}
271+
lenient = true;
272+
this.lenientCreationLock.lock();
273+
try {
274+
this.singletonsInLenientCreation.add(beanName);
275+
}
276+
finally {
277+
this.lenientCreationLock.unlock();
278+
}
260279
}
261280
else {
262281
// No specific locking indication (outside a coordinated bootstrap) and
@@ -285,7 +304,24 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
285304
}
286305
catch (BeanCurrentlyInCreationException ex) {
287306
if (locked) {
288-
throw ex;
307+
this.lenientCreationLock.lock();
308+
try {
309+
while ((singletonObject = this.singletonObjects.get(beanName)) == null) {
310+
if (!this.singletonsInLenientCreation.contains(beanName)) {
311+
throw ex;
312+
}
313+
try {
314+
this.lenientCreationFinished.await();
315+
}
316+
catch (InterruptedException ie) {
317+
Thread.currentThread().interrupt();
318+
}
319+
}
320+
return singletonObject;
321+
}
322+
finally {
323+
this.lenientCreationLock.unlock();
324+
}
289325
}
290326
// Try late locking for waiting on specific bean to be finished.
291327
this.singletonLock.lock();
@@ -339,6 +375,16 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
339375
if (locked) {
340376
this.singletonLock.unlock();
341377
}
378+
if (lenient) {
379+
this.lenientCreationLock.lock();
380+
try {
381+
this.singletonsInLenientCreation.remove(beanName);
382+
this.lenientCreationFinished.signalAll();
383+
}
384+
finally {
385+
this.lenientCreationLock.unlock();
386+
}
387+
}
342388
}
343389
}
344390

spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java

+55-6
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,12 @@
1919
import org.junit.jupiter.api.Test;
2020
import org.junit.jupiter.api.Timeout;
2121

22-
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
2322
import org.springframework.beans.factory.ObjectProvider;
2423
import org.springframework.beans.testfixture.beans.TestBean;
2524
import org.springframework.context.ConfigurableApplicationContext;
2625
import org.springframework.core.testfixture.EnabledForTestGroups;
2726
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
2827

29-
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3028
import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND;
3129
import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING;
3230

@@ -42,8 +40,19 @@ class BackgroundBootstrapTests {
4240
void bootstrapWithUnmanagedThread() {
4341
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class);
4442
ctx.getBean("testBean1", TestBean.class);
45-
assertThatExceptionOfType(BeanCurrentlyInCreationException.class).isThrownBy( // late - not during refresh
46-
() -> ctx.getBean("testBean2", TestBean.class));
43+
ctx.getBean("testBean2", TestBean.class);
44+
ctx.close();
45+
}
46+
47+
@Test
48+
@Timeout(5)
49+
@EnabledForTestGroups(LONG_RUNNING)
50+
void bootstrapWithUnmanagedThreads() {
51+
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class);
52+
ctx.getBean("testBean1", TestBean.class);
53+
ctx.getBean("testBean2", TestBean.class);
54+
ctx.getBean("testBean3", TestBean.class);
55+
ctx.getBean("testBean4", TestBean.class);
4756
ctx.close();
4857
}
4958

@@ -55,6 +64,7 @@ void bootstrapWithCustomExecutor() {
5564
ctx.getBean("testBean1", TestBean.class);
5665
ctx.getBean("testBean2", TestBean.class);
5766
ctx.getBean("testBean3", TestBean.class);
67+
ctx.getBean("testBean4", TestBean.class);
5868
ctx.close();
5969
}
6070

@@ -87,6 +97,45 @@ public TestBean testBean2() {
8797
}
8898

8999

100+
@Configuration(proxyBeanMethods = false)
101+
static class UnmanagedThreadsBeanConfig {
102+
103+
@Bean
104+
public TestBean testBean1(ObjectProvider<TestBean> testBean3, ObjectProvider<TestBean> testBean4) {
105+
new Thread(testBean3::getObject).start();
106+
new Thread(testBean4::getObject).start();
107+
try {
108+
Thread.sleep(1000);
109+
}
110+
catch (InterruptedException ex) {
111+
throw new RuntimeException(ex);
112+
}
113+
return new TestBean();
114+
}
115+
116+
@Bean
117+
public TestBean testBean2(TestBean testBean4) {
118+
return new TestBean(testBean4);
119+
}
120+
121+
@Bean
122+
public TestBean testBean3(TestBean testBean4) {
123+
return new TestBean(testBean4);
124+
}
125+
126+
@Bean
127+
public TestBean testBean4() {
128+
try {
129+
Thread.sleep(2000);
130+
}
131+
catch (InterruptedException ex) {
132+
throw new RuntimeException(ex);
133+
}
134+
return new TestBean();
135+
}
136+
}
137+
138+
90139
@Configuration(proxyBeanMethods = false)
91140
static class CustomExecutorBeanConfig {
92141

@@ -117,8 +166,8 @@ public TestBean testBean3() {
117166
}
118167

119168
@Bean
120-
public String dependent(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) {
121-
return "";
169+
public TestBean testBean4(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) {
170+
return new TestBean();
122171
}
123172
}
124173

0 commit comments

Comments
 (0)