Unravel โบ Java concepts โบ Concurrency
ThreadLocal and scoped values
A ThreadLocal<T> gives every thread its own private copy of a variable.
Learn these first
At a glance
Each worker has their own locker: the same locker number everybody uses in code, but each worker gets different contents, so nobody has to share or lock.
- 1private static final ThreadLocal<User> CURRENT = new ThreadLocal<>(); then CURRENT.set(user), CURRENT.get() and CURRENT.remove()Each thread sees only its own value.
- 2ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy")) creates a value lazily per thread
- 3Frameworks use it for the security context, the current transaction and logging context (MDC)That is why @Transactional or the logged-in user "disappear" when you switch to another thread.
- 4ScopedValue (a newer API) binds a value for a block of code and, with virtual threads, is cheaper and safer
- In thread pools the threads live on and are reused for other requests: a value that is not removed leaks into the next request (wrong user, stale data) and keeps objects (and class loaders) alive
- A value does not follow work to another thread: tasks you submit to an executor do not see the caller's ThreadLocals unless you copy them in (or use a context-propagating wrapper)
- Each Thread has a ThreadLocalMap with weak keys (the ThreadLocal objects) and strong values
public class RequestContext { private static final ThreadLocal<String> USER = new ThreadLocal<>(); private static final ThreadLocal<java.text.SimpleDateFormat> FMT = ThreadLocal.withInitial(() -> new java.text.SimpleDateFormat("yyyy-MM-dd")); static void run(String user, Runnable work) { USER.set(user); try { work.run(); } finally { USER.remove(); } } static String current() { return USER.get(); } static String today() { return FMT.get().format(new java.util.Date()); }}- line 2 uses ThreadLocal, the core of ThreadLocal and scoped values
- line 3 uses ThreadLocal, the core of ThreadLocal and scoped values
Read the full explanation (about 1 min)
A ThreadLocal<T> gives every thread its own private copy of a variable. It is used for per-request context (user, transaction, trace id) and for non-thread-safe helpers like date formatters.
A mental model
Each worker has their own locker: the same locker number everybody uses in code, but each worker gets different contents, so nobody has to share or lock.
How it works
private static final ThreadLocal<User> CURRENT = new ThreadLocal<>(); then CURRENT.set(user), CURRENT.get() and CURRENT.remove(). Each thread sees only its own value.
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy")) creates a value lazily per thread.
Frameworks use it for the security context, the current transaction and logging context (MDC). That is why @Transactional or the logged-in user "disappear" when you switch to another thread.
ScopedValue (a newer API) binds a value for a block of code and, with virtual threads, is cheaper and safer.
Going deeper
In thread pools the threads live on and are reused for other requests: a value that is not removed leaks into the next request (wrong user, stale data) and keeps objects (and class loaders) alive. Always remove() in a finally.
A value does not follow work to another thread: tasks you submit to an executor do not see the caller's ThreadLocals unless you copy them in (or use a context-propagating wrapper).
With millions of virtual threads, per-thread caches in ThreadLocals become expensive; prefer passing values explicitly or ScopedValue.
Under the hood
Each Thread has a ThreadLocalMap with weak keys (the ThreadLocal objects) and strong values. When the ThreadLocal itself is garbage collected the entry is removed lazily, but the value stays until the map cleans it up, hence the leak risk in long-lived threads.
Common mistakes
- Forgetting
remove()in pooled threads. - Expecting values to propagate to child tasks.
- Using a ThreadLocal as a hidden global.
How Unravel recognises it
When you paste code, Unravel looks for these signs of threadlocal and scoped values. Spotting them yourself is a good habit when you read unfamiliar code:
- Types:
ThreadLocalInheritableThreadLocalScopedValue - Method calls:
withInitial()ThreadLocal.withInitial()
Part of a learning path
- Concurrency beyond the basics ยท previous: Producer-consumer with blocking queues ยท next: Virtual threads
Related concepts
Frequently asked questions
What is ThreadLocal and scoped values?
A ThreadLocal<T> gives every thread its own private copy of a variable. It is used for per-request context (user, transaction, trace id) and for non-thread-safe helpers like date formatters.
What are the most common mistakes with ThreadLocal and scoped values?
Forgetting remove() in pooled threads. Expecting values to propagate to child tasks. Using a ThreadLocal as a hidden global.
How can I see ThreadLocal and scoped values in real code?
Paste your own Java into Unravel and it will point out where threadlocal and scoped values appears, explain how it works at the detail level you choose, and draw the flow of the code around it.