Java ThreadLocal Class
Last modified: April 13, 2025
The java.lang.ThreadLocal
class provides thread-local variables.
These variables differ from normal variables in that each thread has its own
independently initialized copy of the variable. ThreadLocal instances are
typically private static fields in classes.
ThreadLocal is useful when you want to maintain per-thread state without requiring synchronization. Common use cases include storing user sessions, transaction contexts, or other thread-specific data that needs to be isolated from other threads.
ThreadLocal Basics
ThreadLocal provides thread confinement by creating a separate copy of the
variable for each thread. The main methods include get
,
set
, remove
, and initialValue
.
public class ThreadLocal<T> { public T get() {...} public void set(T value) {...} public void remove() {...} protected T initialValue() {...} }
The code above shows the main methods provided by ThreadLocal. The generic type T represents the type of value stored in the thread-local variable.
Basic ThreadLocal Example
This example demonstrates the basic usage of ThreadLocal. We create a simple counter that maintains separate counts for each thread.
package com.zetcode; public class ThreadLocalExample { private static final ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0); public static void main(String[] args) { Runnable task = () -> { int counter = threadLocalCounter.get(); threadLocalCounter.set(counter + 1); System.out.println(Thread.currentThread().getName() + ": " + threadLocalCounter.get()); }; Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); Thread thread3 = new Thread(task); thread1.start(); thread2.start(); thread3.start(); } }
Each thread gets its own copy of the counter initialized to 0. When a thread increments its counter, it doesn't affect counters in other threads. The output shows each thread maintaining its own count independently.
ThreadLocal with InitialValue
This example shows how to override the initialValue
method to
provide a custom initial value for each thread's copy of the variable.
package com.zetcode; public class ThreadLocalInitialValue { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>() { @Override protected String initialValue() { return "Initial-" + Thread.currentThread().getName(); } }; public static void main(String[] args) { Runnable task = () -> { System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); threadLocal.set("Modified-" + Thread.currentThread().getName()); System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); }; new Thread(task, "Thread-1").start(); new Thread(task, "Thread-2").start(); } }
Each thread starts with its own initial value containing the thread name. We then modify the value in each thread independently. The output demonstrates that changes in one thread don't affect other threads' values.
ThreadLocal in Web Applications
ThreadLocal is commonly used in web applications to store per-request data like user sessions or transaction contexts. This example simulates a web request handler.
package com.zetcode; public class WebRequestSimulator { private static final ThreadLocal<String> userSession = new ThreadLocal<>(); static class RequestHandler implements Runnable { private final String userId; RequestHandler(String userId) { this.userId = userId; } @Override public void run() { userSession.set("Session-" + userId); try { System.out.println(Thread.currentThread().getName() + " processing request for user: " + userId); System.out.println("Session ID: " + userSession.get()); Thread.sleep(100); // Simulate request processing } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { userSession.remove(); // Clean up } } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); for (int i = 1; i <= 5; i++) { executor.execute(new RequestHandler("User" + i)); } executor.shutdown(); } }
This example simulates handling web requests in a thread pool. Each request gets
its own session stored in the ThreadLocal. The finally
block
ensures cleanup with remove
to prevent memory leaks in thread
pools.
ThreadLocal with SimpleDateFormat
SimpleDateFormat is not thread-safe, making ThreadLocal an ideal solution for sharing it across multiple threads. This example demonstrates this pattern.
package com.zetcode; public class DateFormatExample { private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); static class DatePrinter implements Runnable { private final Date date; DatePrinter(Date date) { this.date = date; } @Override public void run() { String formattedDate = dateFormat.get().format(date); System.out.println(Thread.currentThread().getName() + ": " + formattedDate); } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); Date today = new Date(); for (int i = 0; i < 5; i++) { executor.execute(new DatePrinter(today)); } executor.shutdown(); } }
Each thread gets its own SimpleDateFormat instance, avoiding synchronization issues. This is more efficient than creating a new SimpleDateFormat for each format operation or synchronizing access to a shared instance.
ThreadLocal for Context Passing
ThreadLocal can be used to pass context through multiple method calls without explicitly passing parameters. This example shows a transaction context.
package com.zetcode; public class TransactionContext { private static final ThreadLocal<String> transactionId = new ThreadLocal<>(); static void startTransaction(String id) { transactionId.set(id); System.out.println("Started transaction: " + id); } static void processTransaction() { System.out.println("Processing transaction: " + transactionId.get()); } static void commitTransaction() { System.out.println("Committing transaction: " + transactionId.get()); transactionId.remove(); } static class TransactionTask implements Runnable { private final String id; TransactionTask(String id) { this.id = id; } @Override public void run() { try { startTransaction(id); processTransaction(); commitTransaction(); } catch (Exception e) { transactionId.remove(); // Clean up on error } } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new TransactionTask("TXN-001")); executor.execute(new TransactionTask("TXN-002")); executor.shutdown(); } }
The transaction ID is stored in ThreadLocal and accessed by multiple methods without being passed as a parameter. Each thread maintains its own transaction context, and the ID is properly cleaned up after the transaction completes.
ThreadLocal with InheritableThreadLocal
InheritableThreadLocal allows child threads to inherit values from parent threads. This example demonstrates this behavior.
package com.zetcode; public class InheritableThreadLocalExample { private static final ThreadLocal<String> regularThreadLocal = new ThreadLocal<>(); private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { regularThreadLocal.set("Parent Thread Value"); inheritableThreadLocal.set("Parent Thread Value"); Thread childThread = new Thread(() -> { System.out.println("Regular ThreadLocal in child: " + regularThreadLocal.get()); System.out.println("InheritableThreadLocal in child: " + inheritableThreadLocal.get()); }); childThread.start(); } }
The child thread can access the value set in the parent thread through InheritableThreadLocal but not through regular ThreadLocal. This is useful when you need to pass context to child threads while maintaining thread safety.
ThreadLocal Memory Leak Prevention
ThreadLocal can cause memory leaks if not used properly, especially in thread pools. This example shows proper cleanup.
package com.zetcode; public class ThreadLocalCleanup { private static final ThreadLocal<byte[]> bigData = new ThreadLocal<>(); static class Task implements Runnable { @Override public void run() { try { // Store large data in ThreadLocal bigData.set(new byte[10 * 1024 * 1024]); // 10MB System.out.println("Stored data in " + Thread.currentThread().getName()); } finally { // Critical: remove to prevent memory leak bigData.remove(); System.out.println("Cleaned up in " + Thread.currentThread().getName()); } } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); for (int i = 0; i < 5; i++) { executor.execute(new Task()); } executor.shutdown(); } }
This example demonstrates proper cleanup of ThreadLocal variables. The
finally
block ensures removal even if an exception occurs. Without
this cleanup, thread pool threads would retain references to the large byte
arrays, causing memory leaks.
Source
Java ThreadLocal Class Documentation
In this article, we've covered the Java ThreadLocal class with practical examples. ThreadLocal is a powerful tool for managing per-thread state without synchronization, but must be used carefully to avoid memory leaks.
Author
List all Java tutorials.