ThreadLocal in Java: All You Need to Know with Examples

Table of Contents

In multi-threaded Java applications, ThreadLocal provides a way to give each thread its own separate copy of a variable. This means threads do not share data, thus avoiding synchronization issues. In this article, we explain what ThreadLocal is, how it works internally, and when to use it. We cover common use cases, compare ThreadLocal to alternatives like synchronized, discuss its subclass InheritableThreadLocal, explore some Spring Boot use cases of ThreadLocal, highlight common pitfalls (especially memory leaks).

What is ThreadLocal in Java?

ThreadLocal is a Java class that provides thread-local variables. Each thread that accesses a ThreadLocal via its get() or set() methods has its own independent copy of the variable. In other words, a ThreadLocal is like a container whose value is unique per thread.

ThreadLocal Internal Working

Internally, each Thread object has a ThreadLocalMap (essentially a map) that holds the values for all ThreadLocal variables used by that thread.

If you open the OpenJDK source code for java.lang.Thread, you’ll see this field:

 /*
   * ThreadLocal values pertaining to this thread. This map is maintained
   * by the ThreadLocal class.
 */
 private ThreadLocal.ThreadLocalMap threadLocals;

Each Thread has a field called threadLocals (type ThreadLocalMap). This is where all ThreadLocal values for that thread live.

When you call threadLocal.set(value), the current thread’s map stores value under a weak reference key to the ThreadLocal instance.

Inside ThreadLocal.java:

    private void set(Thread t, T value) {
       ThreadLocalMap map = getMap(t);
       if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

// where ThreadLocalMap defined as a static inner class in ThreadLocal.

   static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        
        private Entry[] table;
   }

Set a value to a ThreadLocal variable by following these steps:

  • First, it gets the current thread (Thread.currentThread()).
  • Then, it looks for that thread’s threadLocals map (getMap(t)).
  • If the map exists, it stores the value with map.set(this, value).
  • If not, it creates a new ThreadLocalMap.

So, the value is stored inside the current thread’s private map, not in the ThreadLocal object itself.

When you later call threadLocal.get(), it retrieves that thread-specific value. Because the data is stored per-thread, no two threads see each other’s values, even though they use the same ThreadLocal object.

In practice, you create a ThreadLocal like this:

private static final ThreadLocal<MyContext> CONTEXT = new ThreadLocal<>();
// or using Java 8+:
private static final ThreadLocal<MyContext> CONTEXT = ThreadLocal.withInitial(MyContext::new);

This ensures each thread has its own MyContext object.

When to Use ThreadLocal in Java (Practical Use Cases)

Use ThreadLocal when you need thread-specific data without passing it explicitly through methods or locking. Common scenarios:

  • Per-request context in web applications: In some frameworks, ThreadLocal is used under the covers for contexts. For instance, Spring’s RequestContextHolder uses ThreadLocal to track the current request attributes. Security frameworks (e.g. Spring Security’s SecurityContextHolder) also use ThreadLocal for the current user’s security context.
  • Thread-safe wrappers for non-thread-safe objects: Some objects (e.g. SimpleDateFormat) are not thread-safe. You can give each thread its own instance via ThreadLocal, avoiding expensive locking or creating new instances repeatedly. For example, one thread-safe solution for date formatting is ThreadLocal<SimpleDateFormat> threadFormatter = ThreadLocal.withInitial(() -> new SimpleDateFormat(pattern));.
  • Caching per-thread resources: Heavy objects that are expensive to create (database connections, buffers, etc.) can be stored in ThreadLocal so each thread reuses its own instance. This reduces construction cost across multiple uses by that thread.
  • Avoiding parameter pollution: Sometimes you need to pass a variable through deep call chains, but don’t want to modify every method signature. ThreadLocal lets you “hide” this context.

In short, ThreadLocal is useful when shared static fields aren’t appropriate and you want each thread to have its own independent data. It avoids synchronization overhead because threads never see each other’s copy.

ThreadLocal vs synchronized

Both synchronized and ThreadLocal are tools to deal with concurrency, but they solve different problems.

  • synchronized: Ensures mutual exclusion: only one thread at a time can execute a synchronized block on an object. This is useful when threads must coordinate access to shared data. However, synchronization incurs locking overhead and can lead to contention and blocking if overused.
  • ThreadLocal: Instead of coordinating threads on shared data, ThreadLocal gives each thread its own data. There is no contention because threads do not share the variable.

Key differences: If you need every thread to work with separate copies of a variable (no sharing), ThreadLocal is a natural fit. If you need threads to coordinate access to one shared variable, use synchronization or locks.

ThreadLocal vs InheritableThreadLocal

ThreadLocal vs InheritableThreadLocal

By default, a child thread does not see the parent thread’s ThreadLocal values. When you create a new thread, its ThreadLocalMap is independent of the parent’s. If you need a child thread to inherit some initial value from the parent’s ThreadLocal, use InheritableThreadLocal instead.

InheritableThreadLocal is a subclass that “extends ThreadLocal to provide inheritance of values from parent thread to child thread”. It works by invoking the childValue(parentValue) method when a new thread is spawned. By default, the child’s initial value is the parent’s value (copied). This means modifications to the child’s ThreadLocal do not affect the parent, but the child does start with a copy of the parent’s data.

However, even with InheritableThreadLocal, the underlying mechanism is almost the same as ThreadLocal (each thread has its own map entry).

Example

ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("JavaLaunchpad");

Runnable runnable = () ->  {
    System.out.print(tl.get() + "  " + Thread.currentThread().getName());
};
Thread thread = new Thread(runnable);
thread.start();

The output of this code will be:

null  Thread-0

This happened because the child thread (Thread-0) doesn’t inherit the parent’s ThreadLocal variables.

The solution is to use the InheritableThreadLocal class.

InheritableThreadLocal<String> tl = new InheritableThreadLocal<>();
tl.set("JavaLaunchpad");

Runnable runnable = () ->  {
     System.out.print(tl.get() + "  " + Thread.currentThread().getName());
};

Thread thread = new Thread(runnable);
thread.start();

The output of this code will be:

JavaLaunchpad  Thread-0

Common Pitfalls & Memory Leaks

A major pitfall with ThreadLocal is memory leaks, especially when using thread pools. Since a ThreadLocal value is stored in the thread’s map, if you never remove it, the data can stick around as long as the thread lives. In a container or pool, threads often live indefinitely and are reused for many tasks. An unremoved ThreadLocal value accumulates and wastes memory.

For example, suppose you set a large object in a ThreadLocal and never call remove(). That object stays referenced in the thread’s ThreadLocalMap. Even if the ThreadLocal key is garbage-collected, the value remains because the map holds a strong reference to it. Over time, these “stale” values pile up.

In practice, memory leaks arise when threads are pooled (like in servlet containers, executors, or async frameworks). Each request or task using ThreadLocal must clean up before the thread returns to the pool. Otherwise, the next task on that thread might see leftover data or the old data simply sits unused but uncollected.

How to avoid leaks: Always remove ThreadLocal data when done. A common pattern is:

try {
    threadLocal.set(someValue);
    // ... use threadLocal.get() ...
} finally {
    threadLocal.remove();
}

Using a try-finally ensures cleanup even if exceptions occur.

In summary, never let a ThreadLocal value linger longer than needed. Remove it explicitly, especially in long-lived threads, to prevent OOM errors.

How Spring Boot Uses Java ThreadLocal

Spring Boot internally relies on ThreadLocal in several core classes to manage security and request contexts. This allows per-thread storage of request or authentication data without passing it through method arguments.

SecurityContextHolder

Spring Security stores authentication information per thread using SecurityContextHolder. By default, it uses the MODE_THREADLOCAL strategy, which internally maintains a ThreadLocal<SecurityContext>:

you can check spring securtity source code on github

// SecurityContextHolder.java (simplified view)
private static final ThreadLocal<SecurityContext> contextHolder =
        new ThreadLocal<>();

public static SecurityContext getContext() {
    SecurityContext ctx = contextHolder.get();
    if (ctx == null) {
        ctx = createEmptyContext();
        contextHolder.set(ctx);
    }
    return ctx;
}

public static void setContext(SecurityContext context) {
    contextHolder.set(context);
}

public static void clearContext() {
    contextHolder.remove(); // Prevent memory leaks in thread pools
}

You can check the original class source code in the Spring Security repository on GitHub. start from here.

Usage Example in Application Code:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication != null ? authentication.getName() : "anonymous";

RequestContextHolder

RequestContextHolder provides access to request-specific attributes for the current thread. It internally stores a ThreadLocal<RequestAttributes>:

// RequestContextHolder.java (simplified)
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
        new NamedThreadLocal<>("Request context");

public static RequestAttributes getRequestAttributes() {
    return requestAttributesHolder.get();
}

public static void setRequestAttributes(RequestAttributes attributes) {
    requestAttributesHolder.set(attributes);
}

public static void resetRequestAttributes() {
    requestAttributesHolder.remove(); // Clean up after request
}

You can check the original class source code in the Spring Security repository on GitHub. start from here.

Usage Example:

ServletRequestAttributes attrs =
        (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
    HttpServletRequest request = attrs.getRequest();
    String clientIp = request.getRemoteAddr();
}

Conclusion and Further Reading

ThreadLocal is a powerful Java API for thread-specific variables. When used correctly, it simplifies passing data through layers without synchronization. We’ve covered its purpose, internal workings, use cases, comparisons to alternatives, and real-world Spring examples. By following best practices—especially cleaning up after use—you can avoid pitfalls like memory leaks.

Leave a Reply

Your email address will not be published. Required fields are marked *

Join the Tribe