Q: What is ThreadLocal? Why is it dangerous in thread pools?

Answer:

ThreadLocal<T> = per-thread variable. Each thread gets its own independent copy. Reads/writes never see other threads' values.

Mental Model

Internally each Thread has a ThreadLocalMap. ThreadLocal is the key, value is per-thread.

Common Use Cases

  • Per-request context: user/tenant/correlation ID propagated through call stack.
  • Non-thread-safe utilities: SimpleDateFormat (notoriously not thread-safe).
  • Transaction/session context (Spring uses ThreadLocal heavily).
private static final ThreadLocal<SimpleDateFormat> FMT =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

String today() { return FMT.get().format(new Date()); }

Set / Get / Remove

ThreadLocal<String> userId = new ThreadLocal<>();
userId.set("u-123");
userId.get();      // "u-123" — only on this thread
userId.remove();   // clear

The Thread Pool Problem

Thread pools reuse threads. If you set() on a pooled thread and don't remove(), the next task on that thread inherits the stale value.

ExecutorService pool = Executors.newFixedThreadPool(4);
ThreadLocal<String> tenant = new ThreadLocal<>();

pool.submit(() -> {
    tenant.set("acme");
    doWork();
    // forgot tenant.remove() ⚠️
});

pool.submit(() -> {
    System.out.println(tenant.get());  // "acme" — leaked from previous task!
});

Memory Leak

ThreadLocalMap keys are weak references to the ThreadLocal object. Values are strong references. If the ThreadLocal becomes unreachable but the thread lives on (pool!), the value stays in the map forever until the slot is cleared lazily.

Always Use Try/Finally

public Object handle(Request r) {
    tenant.set(r.tenantId());
    try {
        return processor.run(r);
    } finally {
        tenant.remove();   // critical for pooled threads
    }
}

Spring's MDC / RequestContextHolder

Spring/SLF4J's MDC and RequestContextHolder use ThreadLocal under the hood. Filter clears them after each request — that's why filters wrap chain calls in try/finally.

InheritableThreadLocal

Child thread inherits parent's value at thread creation. Doesn't propagate to thread pool tasks (pool threads created at startup, not per-task).

Modern Alternative: ScopedValue (Java 21+)

final static ScopedValue<String> USER = ScopedValue.newInstance();

ScopedValue.where(USER, "u-123").run(() -> {
    // USER.get() == "u-123" only inside this run
});
// outside the run() — USER not bound

Immutable, no leak risk, designed for virtual threads.

Virtual Threads + ThreadLocal

Virtual threads are cheap (millions). Each carries its own ThreadLocalMap → memory pressure. Prefer ScopedValue in virtual-thread code.