Concurrency

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.

Java 21 (core language)

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