Skip to content

Commit eea6add

Browse files
committed
Avoid lenient locking for additional external bootstrap threads
Includes spring.locking.strict revision to differentiate between true, false, not set. Includes checkFlag accessor on SpringProperties, also used in StatementCreatorUtils. Closes gh-34729 See gh-34303
1 parent 7f2c1f4 commit eea6add

File tree

5 files changed

+163
-23
lines changed

5 files changed

+163
-23
lines changed

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

+42-5
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
133133
* System property that instructs Spring to enforce strict locking during bean creation,
134134
* rather than the mix of strict and lenient locking that 6.2 applies by default. Setting
135135
* this flag to "true" restores 6.1.x style locking in the entire pre-instantiation phase.
136+
* <p>By default, the factory infers strict locking from the encountered thread names:
137+
* If additional threads have names that match the thread prefix of the main bootstrap thread,
138+
* they are considered external (multiple external bootstrap threads calling into the factory)
139+
* and therefore have strict locking applied to them. This inference can be turned off through
140+
* explicitly setting this flag to "false" rather than leaving it unspecified.
136141
* @since 6.2.6
137142
* @see #preInstantiateSingletons()
138143
*/
@@ -157,8 +162,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
157162
private static final Map<String, Reference<DefaultListableBeanFactory>> serializableFactories =
158163
new ConcurrentHashMap<>(8);
159164

160-
/** Whether lenient locking is allowed in this factory. */
161-
private final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME);
165+
/** Whether strict locking is enforced or relaxed in this factory. */
166+
@Nullable
167+
private final Boolean strictLocking = SpringProperties.checkFlag(STRICT_LOCKING_PROPERTY_NAME);
162168

163169
/** Optional id for this factory, for serialization purposes. */
164170
@Nullable
@@ -214,6 +220,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
214220

215221
private volatile boolean preInstantiationPhase;
216222

223+
@Nullable
224+
private volatile String mainThreadPrefix;
225+
217226
private final NamedThreadLocal<PreInstantiation> preInstantiationThread =
218227
new NamedThreadLocal<>("Pre-instantiation thread marker");
219228

@@ -1045,7 +1054,7 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName
10451054
}
10461055
}
10471056
else {
1048-
// Bean intended to be initialized in main bootstrap thread
1057+
// Bean intended to be initialized in main bootstrap thread.
10491058
if (this.preInstantiationThread.get() == PreInstantiation.BACKGROUND) {
10501059
throw new BeanCurrentlyInCreationException(beanName, "Bean marked for mainline initialization " +
10511060
"but requested in background thread - enforce early instantiation in mainline thread " +
@@ -1057,8 +1066,28 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName
10571066
@Override
10581067
@Nullable
10591068
protected Boolean isCurrentThreadAllowedToHoldSingletonLock() {
1060-
return (this.lenientLockingAllowed && this.preInstantiationPhase ?
1061-
this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null);
1069+
if (this.preInstantiationPhase) {
1070+
// We only differentiate in the preInstantiateSingletons phase.
1071+
PreInstantiation preInstantiation = this.preInstantiationThread.get();
1072+
if (preInstantiation != null) {
1073+
// A Spring-managed thread:
1074+
// MAIN is allowed to lock (true) or even forced to lock (null),
1075+
// BACKGROUND is never allowed to lock (false).
1076+
return switch (preInstantiation) {
1077+
case MAIN -> (Boolean.TRUE.equals(this.strictLocking) ? null : true);
1078+
case BACKGROUND -> false;
1079+
};
1080+
}
1081+
if (Boolean.FALSE.equals(this.strictLocking) ||
1082+
(this.strictLocking == null && !getThreadNamePrefix().equals(this.mainThreadPrefix))) {
1083+
// An unmanaged thread (assumed to be application-internal) with lenient locking,
1084+
// and not part of the same thread pool that provided the main bootstrap thread
1085+
// (excluding scenarios where we are hit by multiple external bootstrap threads).
1086+
return true;
1087+
}
1088+
}
1089+
// Traditional behavior: forced to always hold a full lock.
1090+
return null;
10621091
}
10631092

10641093
@Override
@@ -1076,6 +1105,7 @@ public void preInstantiateSingletons() throws BeansException {
10761105

10771106
this.preInstantiationPhase = true;
10781107
this.preInstantiationThread.set(PreInstantiation.MAIN);
1108+
this.mainThreadPrefix = getThreadNamePrefix();
10791109
try {
10801110
for (String beanName : beanNames) {
10811111
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
@@ -1088,6 +1118,7 @@ public void preInstantiateSingletons() throws BeansException {
10881118
}
10891119
}
10901120
finally {
1121+
this.mainThreadPrefix = null;
10911122
this.preInstantiationThread.remove();
10921123
this.preInstantiationPhase = false;
10931124
}
@@ -1183,6 +1214,12 @@ private void instantiateSingleton(String beanName) {
11831214
}
11841215
}
11851216

1217+
private static String getThreadNamePrefix() {
1218+
String name = Thread.currentThread().getName();
1219+
int numberSeparator = name.lastIndexOf('-');
1220+
return (numberSeparator >= 0 ? name.substring(0, numberSeparator) : name);
1221+
}
1222+
11861223

11871224
//---------------------------------------------------------------------
11881225
// Implementation of BeanDefinitionRegistry interface

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

+11-7
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
272272
// Thread-safe exposure is still guaranteed, there is just a risk of collisions
273273
// when triggering creation of other beans as dependencies of the current bean.
274274
if (logger.isInfoEnabled()) {
275-
logger.info("Creating singleton bean '" + beanName + "' in thread \"" +
275+
logger.info("Obtaining singleton bean '" + beanName + "' in thread \"" +
276276
Thread.currentThread().getName() + "\" while other thread holds " +
277277
"singleton lock for other beans " + this.singletonsCurrentlyInCreation);
278278
}
@@ -443,12 +443,16 @@ private boolean checkDependentWaitingThreads(Thread waitingThread, Thread candid
443443

444444
/**
445445
* Determine whether the current thread is allowed to hold the singleton lock.
446-
* <p>By default, any thread may acquire and hold the singleton lock, except
447-
* background threads from {@link DefaultListableBeanFactory#setBootstrapExecutor}.
448-
* @return {@code false} if the current thread is explicitly not allowed to hold
449-
* the lock, {@code true} if it is explicitly allowed to hold the lock but also
450-
* accepts lenient fallback behavior, or {@code null} if there is no specific
451-
* indication (traditional behavior: always holding a full lock)
446+
* <p>By default, all threads are forced to hold a full lock through {@code null}.
447+
* {@link DefaultListableBeanFactory} overrides this to specifically handle its
448+
* threads during the pre-instantiation phase: {@code true} for the main thread,
449+
* {@code false} for managed background threads, and configuration-dependent
450+
* behavior for unmanaged threads.
451+
* @return {@code true} if the current thread is explicitly allowed to hold the
452+
* lock but also accepts lenient fallback behavior, {@code false} if it is
453+
* explicitly not allowed to hold the lock and therefore forced to use lenient
454+
* fallback behavior, or {@code null} if there is no specific indication
455+
* (traditional behavior: forced to always hold a full lock)
452456
* @since 6.2
453457
*/
454458
@Nullable

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

+81-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.context.annotation;
1818

19+
import java.util.concurrent.ExecutorService;
20+
import java.util.concurrent.Executors;
21+
1922
import org.junit.jupiter.api.Test;
2023
import org.junit.jupiter.api.Timeout;
2124

@@ -67,7 +70,7 @@ void bootstrapWithUnmanagedThreads() {
6770
@Test
6871
@Timeout(10)
6972
@EnabledForTestGroups(LONG_RUNNING)
70-
void bootstrapWithStrictLockingThread() {
73+
void bootstrapWithStrictLockingFlag() {
7174
SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME);
7275
try {
7376
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(StrictLockingBeanConfig.class);
@@ -79,6 +82,42 @@ void bootstrapWithStrictLockingThread() {
7982
}
8083
}
8184

85+
@Test
86+
@Timeout(10)
87+
@EnabledForTestGroups(LONG_RUNNING)
88+
void bootstrapWithStrictLockingInferred() throws InterruptedException {
89+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
90+
ctx.register(InferredLockingBeanConfig.class);
91+
ExecutorService threadPool = Executors.newFixedThreadPool(2);
92+
threadPool.submit(() -> ctx.refresh());
93+
Thread.sleep(500);
94+
threadPool.submit(() -> ctx.getBean("testBean2"));
95+
Thread.sleep(1000);
96+
assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isSameAs(ctx.getBean("testBean1"));
97+
ctx.close();
98+
}
99+
100+
@Test
101+
@Timeout(10)
102+
@EnabledForTestGroups(LONG_RUNNING)
103+
void bootstrapWithStrictLockingTurnedOff() throws InterruptedException {
104+
SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, false);
105+
try {
106+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
107+
ctx.register(InferredLockingBeanConfig.class);
108+
ExecutorService threadPool = Executors.newFixedThreadPool(2);
109+
threadPool.submit(() -> ctx.refresh());
110+
Thread.sleep(500);
111+
threadPool.submit(() -> ctx.getBean("testBean2"));
112+
Thread.sleep(1000);
113+
assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isNull();
114+
ctx.close();
115+
}
116+
finally {
117+
SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null);
118+
}
119+
}
120+
82121
@Test
83122
@Timeout(10)
84123
@EnabledForTestGroups(LONG_RUNNING)
@@ -128,6 +167,24 @@ void bootstrapWithCustomExecutor() {
128167
ctx.close();
129168
}
130169

170+
@Test
171+
@Timeout(10)
172+
@EnabledForTestGroups(LONG_RUNNING)
173+
void bootstrapWithCustomExecutorAndStrictLocking() {
174+
SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME);
175+
try {
176+
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class);
177+
ctx.getBean("testBean1", TestBean.class);
178+
ctx.getBean("testBean2", TestBean.class);
179+
ctx.getBean("testBean3", TestBean.class);
180+
ctx.getBean("testBean4", TestBean.class);
181+
ctx.close();
182+
}
183+
finally {
184+
SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null);
185+
}
186+
}
187+
131188

132189
@Configuration(proxyBeanMethods = false)
133190
static class UnmanagedThreadBeanConfig {
@@ -220,6 +277,27 @@ public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) {
220277
}
221278

222279

280+
@Configuration(proxyBeanMethods = false)
281+
static class InferredLockingBeanConfig {
282+
283+
@Bean
284+
public TestBean testBean1() {
285+
try {
286+
Thread.sleep(1000);
287+
}
288+
catch (InterruptedException ex) {
289+
Thread.currentThread().interrupt();
290+
}
291+
return new TestBean("testBean1");
292+
}
293+
294+
@Bean
295+
public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) {
296+
return new TestBean((TestBean) beanFactory.getSingleton("testBean1"));
297+
}
298+
}
299+
300+
223301
@Configuration(proxyBeanMethods = false)
224302
static class CircularReferenceAgainstMainThreadBeanConfig {
225303

@@ -377,13 +455,13 @@ public ThreadPoolTaskExecutor bootstrapExecutor() {
377455

378456
@Bean(bootstrap = BACKGROUND) @DependsOn("testBean3")
379457
public TestBean testBean1(TestBean testBean3) throws InterruptedException {
380-
Thread.sleep(3000);
458+
Thread.sleep(6000);
381459
return new TestBean();
382460
}
383461

384462
@Bean(bootstrap = BACKGROUND) @Lazy
385463
public TestBean testBean2() throws InterruptedException {
386-
Thread.sleep(3000);
464+
Thread.sleep(6000);
387465
return new TestBean();
388466
}
389467

Diff for: spring-core/src/main/java/org/springframework/core/SpringProperties.java

+27-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,18 @@ public static String getProperty(String key) {
118118
* @param key the property key
119119
*/
120120
public static void setFlag(String key) {
121-
localProperties.put(key, Boolean.TRUE.toString());
121+
localProperties.setProperty(key, Boolean.TRUE.toString());
122+
}
123+
124+
/**
125+
* Programmatically set a local flag to the given value, overriding
126+
* an entry in the {@code spring.properties} file (if any).
127+
* @param key the property key
128+
* @param value the associated boolean value
129+
* @since 6.2.6
130+
*/
131+
public static void setFlag(String key, boolean value) {
132+
localProperties.setProperty(key, Boolean.toString(value));
122133
}
123134

124135
/**
@@ -131,4 +142,19 @@ public static boolean getFlag(String key) {
131142
return Boolean.parseBoolean(getProperty(key));
132143
}
133144

145+
/**
146+
* Retrieve the flag for the given property key, returning {@code null}
147+
* instead of {@code false} in case of no actual flag set.
148+
* @param key the property key
149+
* @return {@code true} if the property is set to the string "true"
150+
* (ignoring case), {@code} false if it is set to any other value,
151+
* {@code null} if it is not set at all
152+
* @since 6.2.6
153+
*/
154+
@Nullable
155+
public static Boolean checkFlag(String key) {
156+
String flag = getProperty(key);
157+
return (flag != null ? Boolean.valueOf(flag) : null);
158+
}
159+
134160
}

Diff for: spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -86,7 +86,7 @@ public abstract class StatementCreatorUtils {
8686
private static final Map<Class<?>, Integer> javaTypeToSqlTypeMap = new HashMap<>(64);
8787

8888
@Nullable
89-
static Boolean shouldIgnoreGetParameterType;
89+
static Boolean shouldIgnoreGetParameterType = SpringProperties.checkFlag(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME);
9090

9191
static {
9292
javaTypeToSqlTypeMap.put(boolean.class, Types.BOOLEAN);
@@ -115,11 +115,6 @@ public abstract class StatementCreatorUtils {
115115
javaTypeToSqlTypeMap.put(java.sql.Timestamp.class, Types.TIMESTAMP);
116116
javaTypeToSqlTypeMap.put(Blob.class, Types.BLOB);
117117
javaTypeToSqlTypeMap.put(Clob.class, Types.CLOB);
118-
119-
String flag = SpringProperties.getProperty(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME);
120-
if (flag != null) {
121-
shouldIgnoreGetParameterType = Boolean.valueOf(flag);
122-
}
123118
}
124119

125120

0 commit comments

Comments
 (0)