ThreadLocal in Java

Naveen AnamaneniUpdated · 4 min read

ThreadLocal<T> is a class that gives each thread its own separate copy of a variable. When one thread calls set, only that thread sees the new value; other threads keep their own copies. It is used for objects that aren't thread-safe but are expensive to create, such as some formatters, and for carrying per-request information, such as the current user, through a call chain without passing it as a parameter. In this lesson, you will learn its methods, a common use, and the memory-leak risk in thread pools.

Methods

MethodDescription
ThreadLocal.withInitial(supplier)It is used to create a thread-local whose first get() in each thread uses the supplier.
get()It returns the current thread's value.
set(value)It sets the current thread's value.
remove()It removes the current thread's value.

Example: One Copy per Thread

Let's see an example where three threads each keep their own counter in the same ThreadLocal.

PerThread.java▶ Try it
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
 
public class PerThread {
    static final ThreadLocal<Integer> counter =
        ThreadLocal.withInitial(() -> 0);
 
    public static void main(String[] args) throws InterruptedException {
        Map<String, Integer> results = new ConcurrentHashMap<>();
        List<Thread> threads = List.of(
            new Thread(() -> work(3, results), "Navika"),
            new Thread(() -> work(5, results), "Srihitha"),
            new Thread(() -> work(2, results), "Advitha"));
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
 
        System.out.println(new TreeMap<>(results));
        System.out.println("main's own counter: " + counter.get());
    }
 
    static void work(int times, Map<String, Integer> results) {
        for (int i = 0; i < times; i++) {
            counter.set(counter.get() + 1);
        }
        results.put(Thread.currentThread().getName(), counter.get());
        counter.remove();
    }
}
Output
{Advitha=2, Navika=3, Srihitha=5}
main's own counter: 0

Explanation: All three threads use the same counter object, yet each sees only its own value, so the counts never mix and no locking is needed. The main thread never set a value, so its get() returns the initial 0. Each worker calls remove() when it's done.

Example: Request Context

RequestContext.java▶ Try it
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class RequestContext {
    static final ThreadLocal<String> currentUser = new ThreadLocal<>();
 
    static void handle(String user) {
        currentUser.set(user);
        try {
            saveOrder();
        } finally {
            currentUser.remove();
        }
    }
 
    static void saveOrder() {
        audit("order saved");
    }
 
    static void audit(String action) {
        System.out.println(action + " by " + currentUser.get());
    }
 
    public static void main(String[] args) {
        try (ExecutorService pool = Executors.newSingleThreadExecutor()) {
            pool.submit(() -> handle("Navika"));
            pool.submit(() -> handle("Srihitha"));
            pool.submit(() -> audit("background job"));
        }
    }
}
Output
order saved by Navika
order saved by Srihitha
background job by null

Explanation: handle stores the user, and audit, two calls deeper, reads it without any parameter being passed. All three tasks run on the same pool thread. Because handle calls remove() in finally, the third task sees null, not Srihitha's name. Without remove(), it would have wrongly reported "background job by Srihitha".

Common Mistakes

Forgetting remove() in pooled threads. Data leaks between tasks and memory is never freed.

Expecting child threads to see the value. They don't; InheritableThreadLocal copies values to new threads.

Using ThreadLocal to share data between threads. It does the opposite: it keeps data apart.

Interview Corner

What is ThreadLocal, and what is its main risk?

ThreadLocal provides a separate value of a variable for each thread that accesses it, via get, set and remove, often initialized with withInitial. It's used for per-thread state such as non-thread-safe helpers or request context. The main risk is in thread pools: threads are reused and long-lived, so values not removed leak into later tasks and can cause memory leaks. Always call remove() in a finally block when the work is done.

Practice

  1. Easy: Give each of three threads its own StringBuilder through ThreadLocal.withInitial and build a different sentence in each.
  2. Harder: Remove the currentUser.remove() call from RequestContext, run it, and explain the wrong output.

Quick check: ThreadLocal

4 questions
1What does ThreadLocal give each thread?
2What does get() return in a thread that never called set()?
3Why call remove() in thread pools?
4Do child threads see their parent's ThreadLocal values?

Key Takeaways

Was this tutorial helpful?

Lesson 427 of 482