ThreadLocal
Give each thread a private copy of a value, and learn why cleanup is not optional.
Open this lesson in the learning hubKey points
ThreadLocalstores 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 afinally, or in a filter or interceptor for web apps. InheritableThreadLocalcopies 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.