Skip to content

Conversation

@chschu
Copy link
Contributor

@chschu chschu commented Feb 10, 2026

After updating from Spring Framework v6.2.11 to v6.2.15, we observed heavy lock congestion even under moderate load. The following stacktrace shows the culprit:

"https-jsse-nio-31030-exec-13" - Thread t@166
   java.lang.Thread.State: WAITING
	at java.base@25.0.1/jdk.internal.misc.Unsafe.park(Native Method)
	- parking to wait for <3441435a> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
	at java.base@25.0.1/java.util.concurrent.locks.LockSupport.park(LockSupport.java:223)
	at java.base@25.0.1/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:790)
	at java.base@25.0.1/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1030)
	at java.base@25.0.1/java.util.concurrent.locks.ReentrantLock$Sync.lock(ReentrantLock.java:154)
	at java.base@25.0.1/java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:323)
	at org.springframework.util.ConcurrentReferenceHashMap$Segment.doTask(ConcurrentReferenceHashMap.java:675)
	at org.springframework.util.ConcurrentReferenceHashMap.doTask(ConcurrentReferenceHashMap.java:570)
	at org.springframework.util.ConcurrentReferenceHashMap.computeIfAbsent(ConcurrentReferenceHashMap.java:389)
	at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:230)
	at org.springframework.core.annotation.AnnotationTypeMappings.forAnnotationType(AnnotationTypeMappings.java:206)
	at org.springframework.core.annotation.TypeMappedAnnotations$MergedAnnotationFinder.process(TypeMappedAnnotations.java:426)
	at org.springframework.core.annotation.TypeMappedAnnotations$MergedAnnotationFinder.doWithAnnotations(TypeMappedAnnotations.java:406)
	at org.springframework.core.annotation.TypeMappedAnnotations$MergedAnnotationFinder.doWithAnnotations(TypeMappedAnnotations.java:372)
	at org.springframework.core.annotation.AnnotationsScanner.processMethodAnnotations(AnnotationsScanner.java:395)
	at org.springframework.core.annotation.AnnotationsScanner.processMethodHierarchy(AnnotationsScanner.java:282)
	at org.springframework.core.annotation.AnnotationsScanner.processMethod(AnnotationsScanner.java:247)
	at org.springframework.core.annotation.AnnotationsScanner.process(AnnotationsScanner.java:95)
	at org.springframework.core.annotation.AnnotationsScanner.scan(AnnotationsScanner.java:82)
	at org.springframework.core.annotation.TypeMappedAnnotations.scan(TypeMappedAnnotations.java:248)
	at org.springframework.core.annotation.TypeMappedAnnotations.get(TypeMappedAnnotations.java:155)
	at org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation(AnnotatedElementUtils.java:648)
	at org.springframework.core.annotation.AnnotatedMethod.getMethodAnnotation(AnnotatedMethod.java:155)
	at org.springframework.core.annotation.AnnotatedMethod$AnnotatedMethodParameter.getMethodAnnotation(AnnotatedMethod.java:286)
	at org.springframework.web.method.annotation.ModelFactory.getNameForReturnValue(ModelFactory.java:271)
	at org.springframework.web.method.annotation.ModelFactory.invokeModelAttributeMethods(ModelFactory.java:153)
	at org.springframework.web.method.annotation.ModelFactory.initModel(ModelFactory.java:111)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:975)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:896)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
	<redacted>

Most of the time, the call to ConcurrentReferenceHashMap.computeIfAbsent(...) is read-only and doesn't need the lock.

The implementations of computeIfAbsent(...) and computeIfPresent(...) have been added in v7.0.0 with commit 0552cdb and backported to v6.2.13 with commit 12dd758.

Please note that this issue is different from the one described in gh-35944, because it doesn't involve a call to AnnotationTypeMappings$Cache.get(...).

This PR adds lock-free checks to these methods to avoid locking in the predominant read-only case. It has been verified to solve the lock congestion issue in our load tests.

The modification drastically improves the performace of the read-only case, but adds some overhead (an entry lookup) in the modifying case. This tradeoff might not be feasible for all usages of ConcurrentReferenceHashMap. A similar issue has been identified (and improved, considering the inherent tradeoff) almost a decade ago in the JDK: https://bugs.openjdk.java.net/browse/JDK-8161372

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Feb 10, 2026
…ion of computeIfAbsent and computeIfPresent

Signed-off-by: Christian Schuster <christian@dnup.de>
@chschu chschu force-pushed the bugfix/annotation-lookup-lock-congestion branch from 16ab7e0 to cbac801 Compare February 10, 2026 16:32
@jhoeller jhoeller self-assigned this Feb 11, 2026
@jhoeller jhoeller added type: regression A bug that is also a regression in: core Issues in core modules (aop, beans, core, context, expression) and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Feb 11, 2026
@jhoeller jhoeller added this to the 7.0.4 milestone Feb 11, 2026
@jhoeller jhoeller added the for: backport-to-6.2.x Marks an issue as a candidate for backport to 6.2.x label Feb 11, 2026
@jhoeller jhoeller added status: backported An issue that has been backported to maintenance branches and removed for: backport-to-6.2.x Marks an issue as a candidate for backport to 6.2.x labels Feb 11, 2026
@jhoeller jhoeller merged commit a9b1d63 into spring-projects:main Feb 11, 2026
5 of 6 checks passed
@jhoeller
Copy link
Contributor

Thanks for the PR - merged for 7.0.4 now. I'll backport this to 6.2.16 as well. Both releases are scheduled for tomorrow.

This tradeoff is entirely feasible for ConcurrentReferenceHashMap specifically which used to have two-step get+putIfAbsent interactions (as inherited from the default method in ConcurrentMap) before anyway.

@jhoeller
Copy link
Contributor

On a related note, we revised this for 7.0.4 toward local annotation caching within our AnnotatedMethod handles, avoiding any access to such global caches to begin with (once resolved): see #36307. That trades off some extra memory usage with faster access paths which is why we intend to keep that part 7.0.x only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: core Issues in core modules (aop, beans, core, context, expression) status: backported An issue that has been backported to maintenance branches type: regression A bug that is also a regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants