Let’s start the discussion with ThreadLocal. ThreadLocal is a mechanism that allows you to associate state with a thread.
For example, instead of retrieving the userId in the controller and passing it through all layers until it reaches the final layer(usually the repository layer that holds the database connection), you can store the userId in a ThreadLocal and access it anywhere within the same thread.
| with and without ThreadLocal in multi threaded environment |
The left side of the above diagram depicts how ThreadLocal is used to store user data for the entire lifetime of the business logic execution.
The Spring Framework uses ThreadLocal to store security data related to authentication and authorisation. It is a mechanism to decouple the security aspects from the core business logic of the application.
Let's explore further on the Spring SecurityContext
Spring SecurityContextHolder
SecurityConfig: This is responsible for checking each request and check if they are authenticated
.anyRequest().authenticated()); looks into the SecurityContextHolder (the ThreadLocal storage) to see if there is a valid Authentication object there.
The issues with ThreadLocal
1. Unconstrained mutability
2. Unbounded lifetime
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
try {
this.securityContextHolderStrategy.setDeferredContext(deferredContext);
chain.doFilter(request, response);
}
finally {
this.securityContextHolderStrategy.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}Link to the original code in GitHub
(extracted from the github issue)protected LogBuilder getLogBuilder(final Level level) {
final SLF4JLogBuilder builder = logBuilder.get();
return Constants.ENABLE_THREADLOCALS && !builder.isInUse()
? builder.reset(this, level)
: new SLF4JLogBuilder(this, level);
}
11-Jul-2025 15:29:48.796 SEVERE [Catalina-utility-5] org.apache.catalina.loader.WebappClassLoaderBase.checkThreadLocalMapForLeaks
The web application [<app name>] created a ThreadLocal with key of type [java.lang.ThreadLocal.SuppliedThreadLocal]
(value [java.lang.ThreadLocal$SuppliedThreadLocal@22437c61]) and a value of type
[org.apache.logging.slf4j.SLF4JLogBuilder] (value [org.apache.logging.slf4j.SLF4JLogBuilder@2be61ed4])
but failed to remove it when the web application was stopped.
Threads are going to be renewed over time to try and avoid a probable memory leak.
These are the logs from the application, showing that when the web application was stopped, the thread was not removed because it was still holding a reference to the object.
3. Expensive inheritance
The overhead of thread-local variables may be worse when using a large number of threads because thread-local variables of a parent thread can be inherited by child threads.
In Spring, when an application wants threads spawned by a secure thread to assume the same security identity, this is achieved by using SecurityContextHolder.MODE_INHERITABLETHREADLOCAL. The SecurityContextHolder.MODE_GLOBAL strategy is for applications that want all threads to share the same context, such as Swing applications.
What is a better alternative to ThreadLocal?
With the introduction of virtual threads, which are short-lived, lightweight threads that can run on platform threads and also support ThreadLocal, the memory leak issues become less significant. However, when you have millions of virtual threads, ThreadLocal memory leaks can still become an issue.- A solution that provides a way to maintain inheritable per-thread data for thousands or millions of virtual threads.
- A solution that supports per-thread immutable variables that can be efficiently shared by child threads.
- A solution that ensures the lifetime of these per-thread variables is bounded; any data shared via a per-thread variable should become unusable once the method that initially shared the data has finished.
Scoped Values
A scoped value is a container object that allows a data value to be safely and efficiently shared by a method with its direct and indirect callees within the same thread, and with child threads, without resorting to method parameters. [https://openjdk.org/jeps/506]How does it work? Code calls ScopedValue.where, presenting a scoped value and the object to which it is to be bound. A chained call of the run method binds the scoped value, providing a copy that is specific to the current thread, and then runs the lambda expression passed as an argument. During the lifetime of the run call, the lambda expression, or any method called directly or indirectly from that expression, can read the scoped value via the value’s get method. After the run method finishes, the binding is destroyed.
record Request(String apiKey) { }
record Response() {
void write(String msg) {
System.out.println("Response: " + msg);
}
}
record FrameworkContext(String user) { }
record PersistedObject(String data) { }
class Database {
PersistedObject readKey(String key, String user) {
return new PersistedObject("Data for [" + key + "] accessed by user [" + user + "]");
}
}
class Application {
static void handle(Request request, Response response) {
System.out.println("-> Application: Handling request...");
Framework framework = new Framework();
PersistedObject result = framework.readKey("secret_dashboard_data"); // (4)
response.write(result.data());
}
}
class Framework {
private static final ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance(); // (1)
void serve(Request request, Response response) {
System.out.println("-> Framework: Preparing context...");
var context = new FrameworkContext("Alice_Admin");
ScopedValue.where(CONTEXT, context) // (2)
.run(() -> Application.handle(request, response));
// CONTEXT.get(); throws NoSuchElementException here (2)(a)
System.out.println("-> Framework: Request finished.");
}
public PersistedObject readKey(String key) { // (4)(a)
FrameworkContext context = CONTEXT.get(); // (3)
Database db = new Database();
return db.readKey(key, context.user());
}
}
public class ScopedValueDemo {
public static void main(String[] args) {
Framework myFramework = new Framework();
Request req = new Request("xyz-123-token");
Response res = new Response();
myFramework.serve(req, res);
}
}
- Check (1) in the code: Create a private static final ScopedValue so that it cannot be directly accessed by code in other classes.
- Check (2) in the code: The scoped value (context) passed to run is bound to the corresponding object for the lifetime of the run call. The run method provides one-way sharing of data from the serve method to the readKey method. The scoped value passed to run is bound to the corresponding object for the lifetime of the run call, so CONTEXT.get() in any method called from run will read that value.
- Check (2)(a) in the code: After the run block is done, CONTEXT is no longer available in the thread, and if you try to access it, it will throw a NoSuchElementException. That means manually clearing the context when using ThreadLocal is not required—it is cleared automatically.
- Check (4) and (4)(a) in the code: User code can call Framework.readKey to read the value written by Framework.serve earlier in the thread. Framework.readKey can only be called within the run block since CONTEXT is bound to it. Because the CONTEXT field has private access, user code can only access it via a method in Framework. Since scoped values have no set methods, they cannot be overridden by user code. The value that was set is locked for the duration of the run block.
Even though there is no set method, callees might still need to use the same scoped value to communicate a different value to its own callees.
private static final ScopedValue<String> X = ScopedValue.newInstance();
void foo() {
where(X, "hello").run(() -> bar());
}
void bar() {
System.out.println(X.get()); // prints hello
where(X, "goodbye").run(() -> baz());
System.out.println(X.get()); // prints hello
}
void baz() {
System.out.println(X.get()); // prints goodbye
}
- Level 1 (foo): The thread enters foo(). It pushes a frame onto the stack that says: "For anyone below me, X = 'hello'."
- Level 2 (bar): The thread enters bar(). It looks up the stack, sees the foo frame, and prints "hello."
- Level 3 (The Shadow): Inside bar(), you call where(X, "goodbye").run(). This pushes a new special frame onto the stack. It says: "For anyone below me, X = 'goodbye'."
- Level 4 (baz): The thread enters baz(). It looks up, sees the "goodbye" frame first, and prints "goodbye."
- Level 2 (bar): The thread enters bar() again. It looks up the stack, sees the foo frame, and prints "hello" again.
@Override
public Response handle(Request request, Response response) {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<UserInfo> user = scope.fork(() -> readUserInfo()); // (1)
Supplier<List<Offer>> offers = scope.fork(() -> fetchOffers()); // (2)
scope.join() // Wait for both forks
.throwIfFailed(); // Propagate first error if any
return new Response(user.get(), offers.get());
} catch (Exception ex) {
reportError(response, ex);
return null;
}
}
- No unconstrained mutability: Scoped values have no set methods and cannot be overridden by user code. The value that is set is locked for the duration of the run method.
- No unbounded lifetime: A scoped value is bound to the code block inside the run method, and when the code finishes executing, the context is not available outside the run method. Furthermore, StructuredTaskScope ensures that child threads are terminated, so the binding is destroyed before returning to the parent thread.
- No expensive inheritance: StructuredTaskScope enables efficient sharing of the context with child threads without making copies.
As of now, based on my research, I couldn’t find evidence that the Spring Framework will fully switch to scoped values. However, I am sure they will make optimisations gradually.
The end!
.png)
.png)
Comments
Post a Comment