Skip to content

Commit f5d36aa

Browse files
committed
Revert use of Map::computeIfAbsent in thread and tx scopes
Issues gh-25038 and gh-25618 collectively introduced a regression for thread-scoped and transaction-scoped beans. For example, given a thread-scoped bean X that depends on another thread-scoped bean Y, if the names of the beans (when used as map keys) end up in the same bucket within a ConcurrentHashMap AND an attempt is made to retrieve bean X from the ApplicationContext prior to retrieving bean Y, then the use of Map::computeIfAbsent in SimpleThreadScope results in recursive access to the same internal bucket in the map. On Java 8, that scenario simply hangs. On Java 9 and higher, ConcurrentHashMap throws an IllegalStateException pointing out that a "Recursive update" was attempted. In light of these findings, we are reverting the changes made to SimpleThreadScope and SimpleTransactionScope in commits 50a4fda and 148dc95. Closes gh-25801
1 parent a532c52 commit f5d36aa

File tree

4 files changed

+34
-12
lines changed

4 files changed

+34
-12
lines changed

spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package org.springframework.context.support;
1818

19+
import java.util.HashMap;
1920
import java.util.Map;
20-
import java.util.concurrent.ConcurrentHashMap;
2121

2222
import org.apache.commons.logging.Log;
2323
import org.apache.commons.logging.LogFactory;
@@ -59,15 +59,22 @@ public class SimpleThreadScope implements Scope {
5959
new NamedThreadLocal<Map<String, Object>>("SimpleThreadScope") {
6060
@Override
6161
protected Map<String, Object> initialValue() {
62-
return new ConcurrentHashMap<>();
62+
return new HashMap<>();
6363
}
6464
};
6565

6666

6767
@Override
6868
public Object get(String name, ObjectFactory<?> objectFactory) {
6969
Map<String, Object> scope = this.threadScope.get();
70-
return scope.computeIfAbsent(name, k -> objectFactory.getObject());
70+
// NOTE: Do NOT modify the following to use Map::computeIfAbsent. For details,
71+
// see https://github.com/spring-projects/spring-framework/issues/25801.
72+
Object scopedObject = scope.get(name);
73+
if (scopedObject == null) {
74+
scopedObject = objectFactory.getObject();
75+
scope.put(name, scopedObject);
76+
}
77+
return scopedObject;
7178
}
7279

7380
@Override

spring-context/src/test/java/org/springframework/context/support/SimpleThreadScopeTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class SimpleThreadScopeTests {
3838

3939
@Test
4040
void getFromScope() throws Exception {
41-
String name = "threadScopedObject";
41+
String name = "removeNodeStatusScreen";
4242
TestBean bean = this.applicationContext.getBean(name, TestBean.class);
4343
assertThat(bean).isNotNull();
4444
assertThat(this.applicationContext.getBean(name)).isSameAs(bean);
@@ -50,8 +50,8 @@ void getFromScope() throws Exception {
5050
void getMultipleInstances() throws Exception {
5151
// Arrange
5252
TestBean[] beans = new TestBean[2];
53-
Thread thread1 = new Thread(() -> beans[0] = applicationContext.getBean("threadScopedObject", TestBean.class));
54-
Thread thread2 = new Thread(() -> beans[1] = applicationContext.getBean("threadScopedObject", TestBean.class));
53+
Thread thread1 = new Thread(() -> beans[0] = applicationContext.getBean("removeNodeStatusScreen", TestBean.class));
54+
Thread thread2 = new Thread(() -> beans[1] = applicationContext.getBean("removeNodeStatusScreen", TestBean.class));
5555
// Act
5656
thread1.start();
5757
thread2.start();

spring-context/src/test/resources/org/springframework/context/support/simpleThreadScopeTests.xml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@
1212
</property>
1313
</bean>
1414

15-
<bean id="threadScopedObject" class="org.springframework.beans.testfixture.beans.TestBean" scope="thread">
16-
<property name="spouse" ref="threadScopedObject2" />
15+
<!--
16+
NOTE: The bean names removeNodeStatusScreen and removeNodeStatusPresenter are seemingly
17+
quite odd for TestBean instances; however, these have been chosen due to the fact that
18+
they end up in the same bucket within a HashMap/ConcurrentHashMap initialized with the
19+
default initial capacity.
20+
21+
For details see: https://github.com/spring-projects/spring-framework/issues/25801
22+
-->
23+
<bean id="removeNodeStatusScreen" class="org.springframework.beans.testfixture.beans.TestBean" scope="thread">
24+
<property name="spouse" ref="removeNodeStatusPresenter" />
1725
</bean>
1826

19-
<bean id="threadScopedObject2" class="org.springframework.beans.testfixture.beans.TestBean" scope="thread" />
27+
<bean id="removeNodeStatusPresenter" class="org.springframework.beans.testfixture.beans.TestBean" scope="thread" />
2028

2129
</beans>

spring-tx/src/main/java/org/springframework/transaction/support/SimpleTransactionScope.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
package org.springframework.transaction.support;
1818

19+
import java.util.HashMap;
1920
import java.util.LinkedHashMap;
2021
import java.util.Map;
21-
import java.util.concurrent.ConcurrentHashMap;
2222

2323
import org.springframework.beans.factory.ObjectFactory;
2424
import org.springframework.beans.factory.config.Scope;
@@ -50,7 +50,14 @@ public Object get(String name, ObjectFactory<?> objectFactory) {
5050
TransactionSynchronizationManager.registerSynchronization(new CleanupSynchronization(scopedObjects));
5151
TransactionSynchronizationManager.bindResource(this, scopedObjects);
5252
}
53-
return scopedObjects.scopedInstances.computeIfAbsent(name, k -> objectFactory.getObject());
53+
// NOTE: Do NOT modify the following to use Map::computeIfAbsent. For details,
54+
// see https://github.com/spring-projects/spring-framework/issues/25801.
55+
Object scopedObject = scopedObjects.scopedInstances.get(name);
56+
if (scopedObject == null) {
57+
scopedObject = objectFactory.getObject();
58+
scopedObjects.scopedInstances.put(name, scopedObject);
59+
}
60+
return scopedObject;
5461
}
5562

5663
@Override
@@ -92,7 +99,7 @@ public String getConversationId() {
9299
*/
93100
static class ScopedObjectsHolder {
94101

95-
final Map<String, Object> scopedInstances = new ConcurrentHashMap<>();
102+
final Map<String, Object> scopedInstances = new HashMap<>();
96103

97104
final Map<String, Runnable> destructionCallbacks = new LinkedHashMap<>();
98105
}

0 commit comments

Comments
 (0)