Skip to content

Commit 4635419

Browse files
committed
Enforce circular reference exception between all thread variations
Closes gh-34672
1 parent 4510b78 commit 4635419

File tree

2 files changed

+146
-27
lines changed

2 files changed

+146
-27
lines changed

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

+38-18
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.beans.factory.support;
1818

1919
import java.util.Collections;
20+
import java.util.HashMap;
2021
import java.util.HashSet;
2122
import java.util.Iterator;
2223
import java.util.LinkedHashMap;
@@ -110,8 +111,11 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
110111
/** Names of beans that are currently in lenient creation. */
111112
private final Set<String> singletonsInLenientCreation = new HashSet<>();
112113

113-
/** Map from bean name to actual creation thread for leniently created beans. */
114-
private final Map<String, Thread> lenientCreationThreads = new ConcurrentHashMap<>();
114+
/** Map from one creation thread waiting on a lenient creation thread. */
115+
private final Map<Thread, Thread> lenientWaitingThreads = new HashMap<>();
116+
117+
/** Map from bean name to actual creation thread for currently created beans. */
118+
private final Map<String, Thread> currentCreationThreads = new ConcurrentHashMap<>();
115119

116120
/** Flag that indicates whether we're currently within destroySingletons. */
117121
private volatile boolean singletonsCurrentlyInDestruction = false;
@@ -253,9 +257,11 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) {
253257
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
254258
Assert.notNull(beanName, "Bean name must not be null");
255259

260+
Thread currentThread = Thread.currentThread();
256261
Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
257262
boolean acquireLock = !Boolean.FALSE.equals(lockFlag);
258263
boolean locked = (acquireLock && this.singletonLock.tryLock());
264+
259265
try {
260266
Object singletonObject = this.singletonObjects.get(beanName);
261267
if (singletonObject == null) {
@@ -307,17 +313,27 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
307313
this.lenientCreationLock.lock();
308314
try {
309315
while ((singletonObject = this.singletonObjects.get(beanName)) == null) {
316+
Thread otherThread = this.currentCreationThreads.get(beanName);
317+
if (otherThread != null && (otherThread == currentThread ||
318+
this.lenientWaitingThreads.get(otherThread) == currentThread)) {
319+
throw ex;
320+
}
310321
if (!this.singletonsInLenientCreation.contains(beanName)) {
311322
break;
312323
}
313-
if (this.lenientCreationThreads.get(beanName) == Thread.currentThread()) {
314-
throw ex;
324+
if (otherThread != null) {
325+
this.lenientWaitingThreads.put(currentThread, otherThread);
315326
}
316327
try {
317328
this.lenientCreationFinished.await();
318329
}
319330
catch (InterruptedException ie) {
320-
Thread.currentThread().interrupt();
331+
currentThread.interrupt();
332+
}
333+
finally {
334+
if (otherThread != null) {
335+
this.lenientWaitingThreads.remove(currentThread);
336+
}
321337
}
322338
}
323339
}
@@ -350,17 +366,12 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
350366
// Leniently created singleton object could have appeared in the meantime.
351367
singletonObject = this.singletonObjects.get(beanName);
352368
if (singletonObject == null) {
353-
if (locked) {
369+
this.currentCreationThreads.put(beanName, currentThread);
370+
try {
354371
singletonObject = singletonFactory.getObject();
355372
}
356-
else {
357-
this.lenientCreationThreads.put(beanName, Thread.currentThread());
358-
try {
359-
singletonObject = singletonFactory.getObject();
360-
}
361-
finally {
362-
this.lenientCreationThreads.remove(beanName);
363-
}
373+
finally {
374+
this.currentCreationThreads.remove(beanName);
364375
}
365376
newSingleton = true;
366377
}
@@ -410,6 +421,8 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
410421
this.lenientCreationLock.lock();
411422
try {
412423
this.singletonsInLenientCreation.remove(beanName);
424+
this.lenientWaitingThreads.entrySet().removeIf(
425+
entry -> entry.getValue() == currentThread);
413426
this.lenientCreationFinished.signalAll();
414427
}
415428
finally {
@@ -724,12 +737,19 @@ public void destroySingleton(String beanName) {
724737
// For an individual destruction, remove the registered instance now.
725738
// As of 6.2, this happens after the current bean's destruction step,
726739
// allowing for late bean retrieval by on-demand suppliers etc.
727-
this.singletonLock.lock();
728-
try {
740+
if (this.currentCreationThreads.get(beanName) == Thread.currentThread()) {
741+
// Local remove after failed creation step -> without singleton lock
742+
// since bean creation may have happened leniently without any lock.
729743
removeSingleton(beanName);
730744
}
731-
finally {
732-
this.singletonLock.unlock();
745+
else {
746+
this.singletonLock.lock();
747+
try {
748+
removeSingleton(beanName);
749+
}
750+
finally {
751+
this.singletonLock.unlock();
752+
}
733753
}
734754
}
735755
}

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

+108-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.junit.jupiter.api.Test;
2020
import org.junit.jupiter.api.Timeout;
2121

22+
import org.springframework.beans.factory.BeanCreationException;
2223
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
2324
import org.springframework.beans.factory.ObjectProvider;
2425
import org.springframework.beans.factory.UnsatisfiedDependencyException;
@@ -42,7 +43,7 @@
4243
class BackgroundBootstrapTests {
4344

4445
@Test
45-
@Timeout(5)
46+
@Timeout(10)
4647
@EnabledForTestGroups(LONG_RUNNING)
4748
void bootstrapWithUnmanagedThread() {
4849
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class);
@@ -52,7 +53,7 @@ void bootstrapWithUnmanagedThread() {
5253
}
5354

5455
@Test
55-
@Timeout(5)
56+
@Timeout(10)
5657
@EnabledForTestGroups(LONG_RUNNING)
5758
void bootstrapWithUnmanagedThreads() {
5859
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class);
@@ -64,7 +65,7 @@ void bootstrapWithUnmanagedThreads() {
6465
}
6566

6667
@Test
67-
@Timeout(5)
68+
@Timeout(10)
6869
@EnabledForTestGroups(LONG_RUNNING)
6970
void bootstrapWithStrictLockingThread() {
7071
SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME);
@@ -79,17 +80,26 @@ void bootstrapWithStrictLockingThread() {
7980
}
8081

8182
@Test
82-
@Timeout(5)
83+
@Timeout(10)
8384
@EnabledForTestGroups(LONG_RUNNING)
84-
void bootstrapWithCircularReference() {
85-
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceBeanConfig.class);
85+
void bootstrapWithCircularReferenceAgainstMainThread() {
86+
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceAgainstMainThreadBeanConfig.class);
8687
ctx.getBean("testBean1", TestBean.class);
8788
ctx.getBean("testBean2", TestBean.class);
8889
ctx.close();
8990
}
9091

9192
@Test
92-
@Timeout(5)
93+
@Timeout(10)
94+
@EnabledForTestGroups(LONG_RUNNING)
95+
void bootstrapWithCircularReferenceWithBlockingMainThread() {
96+
assertThatExceptionOfType(BeanCreationException.class)
97+
.isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceWithBlockingMainThreadBeanConfig.class))
98+
.withRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
99+
}
100+
101+
@Test
102+
@Timeout(10)
93103
@EnabledForTestGroups(LONG_RUNNING)
94104
void bootstrapWithCircularReferenceInSameThread() {
95105
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
@@ -98,7 +108,16 @@ void bootstrapWithCircularReferenceInSameThread() {
98108
}
99109

100110
@Test
101-
@Timeout(5)
111+
@Timeout(10)
112+
@EnabledForTestGroups(LONG_RUNNING)
113+
void bootstrapWithCircularReferenceInMultipleThreads() {
114+
assertThatExceptionOfType(BeanCreationException.class)
115+
.isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInMultipleThreadsBeanConfig.class))
116+
.withRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
117+
}
118+
119+
@Test
120+
@Timeout(10)
102121
@EnabledForTestGroups(LONG_RUNNING)
103122
void bootstrapWithCustomExecutor() {
104123
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class);
@@ -202,7 +221,7 @@ public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) {
202221

203222

204223
@Configuration(proxyBeanMethods = false)
205-
static class CircularReferenceBeanConfig {
224+
static class CircularReferenceAgainstMainThreadBeanConfig {
206225

207226
@Bean
208227
public TestBean testBean1(ObjectProvider<TestBean> testBean2) {
@@ -229,6 +248,46 @@ public TestBean testBean2(TestBean testBean1) {
229248
}
230249

231250

251+
@Configuration(proxyBeanMethods = false)
252+
static class CircularReferenceWithBlockingMainThreadBeanConfig {
253+
254+
@Bean
255+
public TestBean testBean1(ObjectProvider<TestBean> testBean2) {
256+
Thread thread = new Thread(testBean2::getObject);
257+
thread.setUncaughtExceptionHandler((t, ex) -> System.out.println(System.currentTimeMillis() + " " + ex + " " + t));
258+
thread.start();
259+
try {
260+
Thread.sleep(1000);
261+
}
262+
catch (InterruptedException ex) {
263+
throw new RuntimeException(ex);
264+
}
265+
return new TestBean(testBean2.getObject());
266+
}
267+
268+
@Bean
269+
public TestBean testBean2(ObjectProvider<TestBean> testBean1) {
270+
System.out.println(System.currentTimeMillis() + " testBean2 begin " + Thread.currentThread());
271+
try {
272+
Thread.sleep(2000);
273+
}
274+
catch (InterruptedException ex) {
275+
throw new RuntimeException(ex);
276+
}
277+
try {
278+
return new TestBean(testBean1.getObject());
279+
}
280+
catch (RuntimeException ex) {
281+
System.out.println(System.currentTimeMillis() + " testBean2 exception " + Thread.currentThread());
282+
throw ex;
283+
}
284+
finally {
285+
System.out.println(System.currentTimeMillis() + " testBean2 end " + Thread.currentThread());
286+
}
287+
}
288+
}
289+
290+
232291
@Configuration(proxyBeanMethods = false)
233292
static class CircularReferenceInSameThreadBeanConfig {
234293

@@ -262,6 +321,46 @@ public TestBean testBean3(TestBean testBean2) {
262321
}
263322

264323

324+
@Configuration(proxyBeanMethods = false)
325+
static class CircularReferenceInMultipleThreadsBeanConfig {
326+
327+
@Bean
328+
public TestBean testBean1(ObjectProvider<TestBean> testBean2, ObjectProvider<TestBean> testBean3) {
329+
new Thread(testBean2::getObject).start();
330+
new Thread(testBean3::getObject).start();
331+
try {
332+
Thread.sleep(2000);
333+
}
334+
catch (InterruptedException ex) {
335+
throw new RuntimeException(ex);
336+
}
337+
return new TestBean();
338+
}
339+
340+
@Bean
341+
public TestBean testBean2(ObjectProvider<TestBean> testBean3) {
342+
try {
343+
Thread.sleep(1000);
344+
}
345+
catch (InterruptedException ex) {
346+
throw new RuntimeException(ex);
347+
}
348+
return new TestBean(testBean3.getObject());
349+
}
350+
351+
@Bean
352+
public TestBean testBean3(ObjectProvider<TestBean> testBean2) {
353+
try {
354+
Thread.sleep(1000);
355+
}
356+
catch (InterruptedException ex) {
357+
throw new RuntimeException(ex);
358+
}
359+
return new TestBean(testBean2.getObject());
360+
}
361+
}
362+
363+
265364
@Configuration(proxyBeanMethods = false)
266365
static class CustomExecutorBeanConfig {
267366

0 commit comments

Comments
 (0)