ThreadLocal

Multithreading · lesson 14 of 38 · 3 min read

Give each thread a private copy of a value, and learn why cleanup is not optional.

Open this lesson in the learning hub

Key points

  • ThreadLocal stores one value per thread. Nothing is shared, so nothing needs locking.
  • Typical uses: request and trace ids for logging, the current user, and helper objects that aren't thread-safe.
  • Pool threads live as long as the app. Skipping remove() leaks memory and leaks one request’s data into the next.
  • Put remove() in a finally, or in a filter or interceptor for web apps.
  • InheritableThreadLocal copies to threads you create, but not to pooled tasks, so it rarely does what people expect.
  • Java 21 previews ScopedValue: immutable, bound to a block, cleaned up automatically. It targets most ThreadLocal use cases.

Example

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {
    // one independent value per thread, so no sharing and no locking
    static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
    static final ThreadLocal<StringBuilder> LOG = ThreadLocal.withInitial(StringBuilder::new);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        for (int i = 1; i <= 4; i++) {
            int n = i;
            pool.execute(() -> {
                REQUEST_ID.set("req-" + n);
                try {
                    LOG.get().append("handled ").append(REQUEST_ID.get());
                    System.out.println(Thread.currentThread().getName() + " -> " + LOG.get());
                } finally {
                    REQUEST_ID.remove();   // pooled threads get reused: always clean up
                    LOG.remove();
                }
            });
        }
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);

        System.out.println("main sees REQUEST_ID = " + REQUEST_ID.get());
    }
}

Every set() needs a remove(), or a pooled thread remembers too much.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Multithreading course, and every lesson in it is listed on the Multithreading contents page.