BackendBench
Coffee

Unravel โ€บ Java concepts โ€บ Concurrency

Concurrency

ThreadLocal and scoped values

A ThreadLocal<T> gives every thread its own private copy of a variable.

Learn these first

At a glance

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.

What happens, in order
  1. 1private static final ThreadLocal<User> CURRENT = new ThreadLocal<>(); then CURRENT.set(user), CURRENT.get() and CURRENT.remove()Each thread sees only its own value.
  2. 2ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy")) creates a value lazily per thread
  3. 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.
  4. 4ScopedValue (a newer API) binds a value for a block of code and, with virtual threads, is cheaper and safer
Remember
Watch out
Forgetting remove() in pooled threadsExpecting values to propagate to child tasksUsing a ThreadLocal as a hidden global
Example (highlighted lines explain it)
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()); }}
  1. line 2 uses ThreadLocal, the core of ThreadLocal and scoped values
  2. 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:

Part of a learning path

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.