59. ThreadLocal
ThreadLocal<T> gives each thread its own independent copy of a variable โ useful for per-thread context like database connections or user sessions, but a memory-leak trap in thread-pool environments if not explicitly removed.
Explanation
ThreadLocal<T> stores a value per thread. Each thread that accesses the variable via get() gets its own copy, initialised by initialValue() or withInitial(supplier). Writes in one thread are completely invisible to other threads.
Common uses: SimpleDateFormat (not thread-safe), per-request context in web frameworks (user ID, transaction), database connection per thread.
Memory leak trap: in thread pools (like Tomcat), threads are reused across requests. If you call set() without ever calling remove(), the ThreadLocal value lives for the entire lifetime of the thread โ potentially forever. The value also prevents GC of any objects it references. Always call threadLocal.remove() in a finally block (or use frameworks that manage this for you). This is why ScopedValue (Java 21+) was introduced.
Example
public class ThreadLocalDemo {
// Per-thread request context
static final ThreadLocal<String> CURRENT_USER = ThreadLocal.withInitial(() -> "anonymous");
static final ThreadLocal<Long> REQUEST_ID = ThreadLocal.withInitial(() -> 0L);
static void handleRequest(String user, long reqId) throws InterruptedException {
CURRENT_USER.set(user);
REQUEST_ID.set(reqId);
try {
processRequest();
} finally {
// CRUCIAL: always remove in thread-pool environments!
CURRENT_USER.remove();
REQUEST_ID.remove();
}
}
static void processRequest() throws InterruptedException {
Thread.sleep(10); // simulate work
System.out.printf("[req-%d] Processing for user: %s (thread: %s)%n",
REQUEST_ID.get(), CURRENT_USER.get(), Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
// Simulate thread pool handling concurrent requests
var executor = java.util.concurrent.Executors.newFixedThreadPool(3);
for (int i = 0; i < 6; i++) {
final long reqId = i;
final String user = "user" + i;
executor.submit(() -> {
try { handleRequest(user, reqId); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
}
executor.shutdown();
executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
System.out.println("Done. Verify: no cross-thread contamination above.");
}
}Sources
- https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ThreadLocal.html โ ThreadLocal API
- https://openjdk.org/jeps/446 โ JEP 446: Scoped Values (modern alternative)