Java ThreadLocal: Per-thread Context Variables
ThreadLocal
Contents
Basic Usage
ThreadLocalset()get()remove()
// Define a ThreadLocal — usually as a static field
static final ThreadLocal<String> current = new ThreadLocal<>();
// Or with an initial value using withInitial()
static final ThreadLocal<Integer> requestCount =
ThreadLocal.withInitial(() -> 0);
// Each thread gets its own copy
Thread t1 = new Thread(() -> {
current.set("user-alice");
System.out.println(Thread.currentThread().getName() + ": " + current.get());
current.remove(); // always remove when done!
});
Thread t2 = new Thread(() -> {
current.set("user-bob");
System.out.println(Thread.currentThread().getName() + ": " + current.get());
current.remove();
});
t1.start(); t2.start();
// Thread-0: user-alice
// Thread-1: user-bob (independent copies — no synchronization needed)
// get(), set(), remove()
ThreadLocal<List<String>> log = ThreadLocal.withInitial(ArrayList::new);
log.get().add("first");
log.get().add("second");
System.out.println(log.get()); // [first, second]
log.remove(); // clears this thread's copy
Web Request Context Pattern
The classic use case: store the current user/request context in a filter/interceptor, use it throughout the request processing stack, then clean up at the end.
// Request context holder
public class RequestContext {
private static final ThreadLocal<RequestContext> holder =
new ThreadLocal<>();
private final String userId;
private final String requestId;
private final Locale locale;
private RequestContext(String userId, String requestId, Locale locale) {
this.userId = userId;
this.requestId = requestId;
this.locale = locale;
}
public static void set(String userId, String requestId, Locale locale) {
holder.set(new RequestContext(userId, requestId, locale));
}
public static RequestContext get() {
RequestContext ctx = holder.get();
if (ctx == null) throw new IllegalStateException("No request context set");
return ctx;
}
public static void clear() { holder.remove(); }
public String userId() { return userId; }
public String requestId() { return requestId; }
public Locale locale() { return locale; }
}
// Servlet filter sets and clears the context
public class ContextFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
try {
RequestContext.set(
extractUserId(req), UUID.randomUUID().toString(), req.getLocale());
chain.doFilter(req, res);
} finally {
RequestContext.clear(); // MUST clear in finally block
}
}
}
// Any service can access the context without parameter passing
public class OrderService {
public Order createOrder(OrderRequest request) {
String userId = RequestContext.get().userId(); // no parameter needed
String reqId = RequestContext.get().requestId();
// ...
}
}
InheritableThreadLocal
InheritableThreadLocal
static final InheritableThreadLocal<String> tenantId =
new InheritableThreadLocal<>();
// Parent thread sets a value
tenantId.set("acme-corp");
// Child thread inherits the parent's value
Thread child = new Thread(() -> {
System.out.println("Child sees: " + tenantId.get()); // "acme-corp"
tenantId.set("child-tenant"); // only affects child
System.out.println("Child after set: " + tenantId.get()); // "child-tenant"
});
child.start();
child.join();
System.out.println("Parent still: " + tenantId.get()); // "acme-corp"
// Override childValue() to transform the value for child threads
static final InheritableThreadLocal<List<String>> callStack =
new InheritableThreadLocal<>() {
@Override
protected List<String> childValue(List<String> parentValue) {
return new ArrayList<>(parentValue); // deep copy for child
}
};
InheritableThreadLocalScopedValueMemory Leak in Thread Pools
When a thread pool reuses threads, a ThreadLocal
// DANGEROUS — leaks data between tasks in a thread pool
ExecutorService pool = Executors.newFixedThreadPool(4);
static final ThreadLocal<Connection> conn = new ThreadLocal<>();
pool.submit(() -> {
conn.set(dataSource.getConnection());
doWork();
// Forgot conn.remove() — Connection stays in ThreadLocal forever!
// The next task on this thread will see the stale connection!
});
// SAFE — always use try/finally to call remove()
pool.submit(() -> {
conn.set(dataSource.getConnection());
try {
doWork();
} finally {
conn.get().close(); // release resource
conn.remove(); // clear ThreadLocal — MANDATORY in thread pools
}
});
// Utility class for safe ThreadLocal usage
class ThreadLocalScope<T> implements AutoCloseable {
private final ThreadLocal<T> local;
ThreadLocalScope(ThreadLocal<T> local, T value) {
this.local = local;
local.set(value);
}
@Override public void close() { local.remove(); }
}
// Usage with try-with-resources
try (var scope = new ThreadLocalScope<>(currentUser, "alice")) {
processRequest();
} // remove() called automatically
ScopedValue: The Modern Alternative (Java 21+)
ScopedValueThreadLocal
// ScopedValue — immutable, bounded scope, no remove() needed
static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
// Bind a value for the duration of a lambda
ScopedValue.where(CURRENT_USER, "alice").run(() -> {
System.out.println(CURRENT_USER.get()); // "alice"
processOrder(); // CURRENT_USER.get() == "alice" anywhere in the call stack
});
// After the lambda, CURRENT_USER is no longer bound — automatically
// ScopedValue works correctly with virtual threads — no thread-pool leak risk
// It also integrates with StructuredTaskScope — child tasks inherit the binding
ScopedValueThreadLocalThreadLocal