Skip to content

ThreadLocal

1 post with the tag “ThreadLocal”

ThreadLocal Thread Variables

Since JDK 1.2, Java has provided java.lang.ThreadLocal. ThreadLocal offers each thread its own independent variable copy, enabling data isolation between threads. Each thread can access its internal copy variable, thus eliminating thread safety issues. ThreadLocal is also an important utility class for implementing thread context passing. This chapter will introduce ThreadLocal’s APIs, implementation principles, typical applications, and memory leakage issues.

5.1 Common APIs and Usage

The API documentation for ThreadLocal is shown in Figure 5-1 below. The main methods include get, initialValue, remove, set, and withInitial.

Figure 5-1 ThreadLocal’s API

Figure 5-1 ThreadLocal's API

5.1.1 Common APIs

  • initialValue()

ThreadLocal provides two ways of instantiation: extending the ThreadLocal class and overriding the initialValue() method to define initialization logic, or creating an anonymous subclass of ThreadLocal and initializing it in its constructor. Below are example codes for both approaches:

// Method 1: Initialize using the initialValue() method
public class MyThreadLocal extends ThreadLocal<String> {
@Override
protected String initialValue() {
return "Initial Value";
}
}
// Method 2: Create an anonymous subclass and initialize in the constructor
ThreadLocal<String> myThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "Initial Value";
}
};
// Or initialize directly during creation
ThreadLocal<String> myThreadLocal = ThreadLocal.withInitial(() -> "Initial Value");

The withInitial() method, introduced in Java 8, is a simplified constructor that allows assignment using a Lambda expression.

  • get()

To retrieve a value from ThreadLocal, call the get method:

MyThreadLocal myThreadLocal = new MyThreadLocal();
// Get the current thread's local value; initial call triggers initialization
String value = myThreadLocal.get();
  • remove()

To remove a value from ThreadLocal, call the remove method:

myThreadLocal.remove();
  • set()

Set the value of the current thread’s thread-local variable:

myThreadLocal.set("New Value");

5.1.2 Basic Usage

The following example demonstrates the basic usage of ThreadLocal.

public class ThreadLocalExample {```
// Create a ThreadLocal variable to store thread IDs
private static final ThreadLocal<Integer> threadId = new ThreadLocal<>() {
@Override
protected Integer initialValue() {
// Initial value set to current thread's ID
return Thread.currentThread().getId();
}
};
public static void main(String[] args) throws InterruptedException {
// Create and start several threads
for (int i = 0; i < 5; i++) {
new Thread(() -> {
// Get and print current thread's ID
System.out.println("Thread ID: " + threadId.get());
}).start();
}
}
}

In this example, each thread will print its own thread ID rather than other threads’ IDs.

The application scenarios of ThreadLocal mainly fall into two categories:

  • Avoiding the need to pass objects through multiple method layers, breaking hierarchical constraints

For example, a unique traceId in request call chains is needed in many places, and passing it down layer by layer is cumbersome. In such cases, the traceId can be stored in ThreadLocal and retrieved directly where needed.

  • Creating object copies to reduce initialization operations while ensuring thread safety

Scenarios like database connections, Spring transaction management, and SimpleDateFormat for date formatting all use ThreadLocal. This avoids initializing an object in every method while maintaining thread safety in multi-threaded environments.

Here’s code demonstrating how ThreadLocal ensures thread safety when using SimpleDateFormat for date formatting:

public class ThreadLocalDemo {
// Create ThreadLocal
static ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
``` public static void main(String[] args) {
IntStream.range(0, 5).forEach(i -> {
// Create 5 threads, each retrieving SimpleDateFormat from threadLocal and formatting the date
new Thread(() -> {
try {
System.out.println(threadLocal.get().parse("2024-03-29 15:11:07"));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}).start();
});
}
}

5.2 Source Code Analysis

5.2.1 UML Diagram of ThreadLocal Class

Using Intellij Idea’s UML plugin, the ThreadLocal class diagram is drawn as shown in Figure 5-2 below.

Figure 5-2 UML Diagram of ThreadLocal Class

Figure 5-2 UML Diagram of ThreadLocal Class

In Figure 5-2, ThreadLocalMap is a static inner class of ThreadLocal, while Entry is a static inner class of ThreadLocalMap and inherits from the WeakReference class. ThreadLocal has two subclasses: SuppliedThreadLocal and InheritableThreadLocal. The Thread class holds a ThreadLocalMap object.

5.2.2 ThreadLocal Source Code Analysis

  • Field Attributes
// Each ThreadLocal instance has a corresponding threadLocalHashCode
// This value will be used to locate the ThreadLocal's corresponding value in ThreadLocalMap
private final int threadLocalHashCode = nextHashCode();
// Initial value for calculating hash values of ThreadLocal instances
private static AtomicInteger nextHashCode = new AtomicInteger();
// Increment for calculating hash values of ThreadLocal instances
private static final int HASH_INCREMENT = 0x61c88647;

Each ThreadLocal instance has a threadLocalHashCode value, which is calculated from nextHashCode and the constant HASH_INCREMENT.

  • Inner Class SuppliedThreadLocal
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;```
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
// Override initialValue() to set initial value
@Override
protected T initialValue() {
// Supplier cannot be null
return supplier.get();
}
}

SuppliedThreadLocal is a new internal class added in JDK8, which simply extends ThreadLocal’s method for initializing values, allowing the use of Lambda expressions introduced in JDK8 for assignment. Note that the Supplier functional interface does not allow null values. For usage examples, please refer to the demonstration above.

  • Constructor
public ThreadLocal() {
}

As shown, the constructor performs no operations.

  • nextHashCode()
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

Generates the corresponding hashcode when creating a ThreadLocal instance, atomically increasing by HASH_INCREMENT each time.

  • initialValue()
protected T initialValue() {
return null;
}

Returns the initial value set for the current thread’s ThreadLocal. This method is called when the thread first invokes ThreadLocal.get(). If a value has already been set via the set() method, this method won’t be called. Custom implementation is required to achieve tailored operations, meaning you can customize it when you want ThreadLocal to initialize different values in different threads.

  • withInitial()
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}

Lambda expression assignment. Refer to the example above for usage.+ get()

public T get() {
// Get current thread
Thread t = Thread.currentThread();
// Get the ThreadLocalMap held by current thread
ThreadLocalMap map = getMap(t);
if (map != null) {
// Get the Entry corresponding to current ThreadLocal in ThreadLocalMap
ThreadLocalMap.Entry e = map.getEntry(this);
// If not null, get the corresponding value
if (e != null) {
T result = (T)e.value;
return result;
}
}
// Called when map is not initialized or current ThreadLocal's Entry is null
return setInitialValue();
}

Retrieves the ThreadLocalMap object of the current thread’s Thread object and gets the Entry corresponding to the current ThreadLocal. If the ThreadLocalMap hasn’t been initialized or the current ThreadLocal’s Entry is null, it calls setInitialValue(), demonstrating lazy loading where initialization occurs only when needed.

  • setInitialValue()
private T setInitialValue() {
// Call custom initialization method
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// If already initialized, perform set operation
map.set(this, value);
else
// If not initialized, initialize and assign value
createMap(t, value);
return value;
}

Initialization operation that returns the initialized value.

  • set(T value)

The set operation is similar to setInitialValue, except the value is passed in externally.

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
  • remove()```java public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { // Remove via ThreadLocalMap’s remove() method m.remove(this); } }
Removes the Entry corresponding to the current ThreadLocal in the ThreadLocalMap of the current thread. If the current thread calls get() after remove(), it will re-invoke initialValue(). Refer to the get() method above.
+ getMap()
Gets the threadLocals of the thread.
```java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
  • createMap()

Creates (initializes) ThreadLocalMap and sets the initial value via firstValue

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

5.3 Thread Association

5.3.1 Thread Context Loss

ThreadLocal effectively solves the context passing problem within a single thread, but in asynchronous scenarios using multiple threads, thread context can be lost. The following code sets a thread variable in the main thread, then starts a child thread that attempts to retrieve the thread variable’s value.

public class ThreadLocalDemo {
public static ThreadLocal<Integer> context = new ThreadLocal<>();
public static void main(String[] args) {
// Set thread variable value (main thread)
context.set(1000);
// Get value from thread variable
Integer ctx = context.get();
System.out.println("ctx= " + ctx);
// The thread is a child thread created by the main thread
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Integer ctx = context.get();
System.out.println("ctx= " + ctx);
}
});
thread.start();
}
}

The output shows that the child thread cannot access the thread variable set by the main thread:

ctx= 1000
ctx= null
```Judging from the name and purpose of the thread variable, it is expected for the child thread to obtain a null value. However, from the perspective of thread context passing functionality, this does not meet the requirements. Therefore, Java officially provides the `InheritableThreadLocal` subclass of `ThreadLocal` to address the issue of context passing loss when creating new threads.
### 5.3.2 InheritableThreadLocal
When using `ThreadLocal`, child threads cannot access the parent thread's local variables. `InheritableThreadLocal` effectively solves this problem. The source code of `InheritableThreadLocal` is as follows:
```java
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// Receives the value of the parent thread's local variable
// This method is called when the parent thread creates a child thread
protected T childValue(T parentValue) {
// Here, it directly returns the original value
return parentValue;
}
// Uses inheritableThreadLocals to store thread variables
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
// Initializes inheritableThreadLocals
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

Unlike ThreadLocal, when using an InheritableThreadLocal object, variables are stored in inheritableThreadLocals. Below are the definitions of the two variables in the Thread class:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Next, let’s examine how the thread variable copying process is implemented during thread creation:

private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
this.name = name;
``````java
Thread parent = currentThread();
// Security and validation code omitted...
// Standard thread initialization operations
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.target = target;
setPriority(priority);
// Copying thread variable map
if (inheritThreadLocals && parent.inheritableThreadLocals != null){
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
this.stackSize = stackSize;
this.tid = nextThreadID();
}

The thread variable map copying occurs in ThreadLocal.createInheritedMap, which essentially creates a new map and copies the values.

private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// Traverse parent thread's table
for (Entry e : parentTable) {
if (e != null) {
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// Assign value
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}

5.3.3 transmittable-thread-local

JDK’s InheritableThreadLocal class enables value transmission from parent threads to child threads. For thread pool scenarios where threads are created and pooled for reuse, the ThreadLocal value transmission between parent-child threads becomes meaningless. What applications actually need is to transmit the ThreadLocal values from when tasks are submitted to the thread pool to when the tasks are executed.

TransmittableThreadLocal (TTL) is an open-source project by Alibaba that provides ThreadLocal value transmission functionality when using execution components like thread pools that involve pooled and reused threads, solving the context transmission problem in asynchronous execution.

TransmittableThreadLocal inherits from InheritableThreadLocal and has a similar usage pattern. Compared to InheritableThreadLocal, it adds a protected transmitteeValue() method to customize how ThreadLocal values are transmitted from task submission to task execution in thread pools.

5.3.3.1 Simple Usage

  • Parent thread transmitting values to child thread
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// Set in parent thread
context.set("value-set-in-parent");
// =====================================================
// Can be read in child thread, value is "value-set-in-parent"
String value = context.get();

This is actually InheritableThreadLocal’s functionality, which can be achieved using InheritableThreadLocal.

5.3.3.2 Value Transmission in Thread Pools

  • Decorating Runnable and Callable
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// Set in parent thread
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
// Additional processing to generate decorated object ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// =====================================================
// Can be read in Task, value is "value-set-in-parent"
String value = context.get();

The above demonstrates Runnable handling, Callable processing is similar.

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================```markdown
// Set in parent thread
context.set("value-set-in-parent");
Callable call = new CallableTask();
// Additional processing to create decorated object ttlCallable
Callable ttlCallable = TtlCallable.get(call);
executorService.submit(ttlCallable);
// =====================================================
// Can be read in Call, value is "value-set-in-parent"
String value = context.get();
  • Decorating Thread Pools

Eliminates the need to decorate Runnable and Callable each time they are passed to the thread pool, as this logic can be handled within the thread pool. Example:

ExecutorService executorService = ...
// Additional processing to create decorated executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// Set in parent thread
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// =====================================================
// Can be read in Task or Call, value is "value-set-in-parent"
String value = context.get();
  • Using Java Agent to Decorate JDK Thread Pool Implementation Classes

Compared to the SDK approach, this method enables transparent transmission of thread pool context. Business code requires no decoration of Runnable or thread pools, achieving non-intrusive application code.

Usage requires adding a premain agent to the application startup parameters, which modifies thread bytecode before application startup. Integration method:

java -javaagent:path/to/transmittable-thread-local-2.x.y.jar springboot-application.jar

Note: If multiple JavaAgents exist, the transmittable Agent parameter must precede other Agent parameters.

5.4 Memory Leaks

The saying “Water can carry a boat but also overturn it” aptly describes ThreadLocal usage. In practice, the author has encountered numerous ThreadLocal issues, such as memory leaks, dirty data, and thread context loss, particularly in thread pool scenarios where improper usage can easily lead to production incidents.

5.4.1 Causes of Memory Leaks

ThreadLocal memory leaks typically occur due to:

  • ThreadLocal variables not being explicitly removed
  • ThreadLocal variables persistently existing in ThreadLocalMap
Each thread has a ThreadLocalMap, which can store multiple ThreadLocal variables. When a ThreadLocal variable is not removed, the objects it references will remain in the thread's ThreadLocalMap. This causes the ThreadLocalMap to grow excessively large, occupying significant memory space and eventually resulting in a memory leak.
### 5.4.2 Detection and Cleanup of Memory Leaks
Generally, after completing the use of thread variables, the `remove()` method should be called immediately to clean up the variables. It is also advisable to place the `remove()` method in a `finally` block to ensure it is always executed. As shown below:
```java
ThreadLocal<Object> threadlocal = new ThreadLocal<>();
try {
Object value = new Object();
threadlocal.set(value);
// Business logic...
} finally {
// Ensure cleanup is always performed
threadlocal.remove();
}

However, the above approach is only suitable for very simple scenarios. In complex situations, such as when multiple thread variables are involved or thread variables are used in multiple places, this method becomes inadequate. Below, we introduce how open-source middleware detects and cleans up thread variables.

5.4.3 Memory Leak Detection in Tomcat

In previous chapters, we analyzed Tomcat’s process of unloading WAR packages. During WAR package unloading, the stop method of the WAR’s class loader, WebappClassLoaderBase, is called to close and clean up resources. This includes checking whether user-created thread variables have been properly cleared. Let’s examine the code:> Source: apache-tomcat-10.1.13-src/java/org/apache/catalina/loader/WebappClassLoaderBase.java

private void checkThreadLocalsForLeaks() {
// Get all JVM threads
Thread[] threads = getThreads();
try {
// Use reflection to access threadLocals and inheritableThreadLocals
Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Field inheritableThreadLocalsField = Thread.class.getDeclaredField("inheritableThreadLocals");
inheritableThreadLocalsField.setAccessible(true);
// Use reflection to access the table field of ThreadLocalMap
Class<?> tlmClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
// Use reflection to access expungeStaleEntries method, which clears all stale entries
Method expungeStaleEntriesMethod = tlmClass.getDeclaredMethod("expungeStaleEntries");
expungeStaleEntriesMethod.setAccessible(true);
// Iterate through all threads to clear references
for (Thread thread : threads) {
Object threadLocalMap;
if (thread != null) {```
// Clear objects referenced by the threadLocalsField
threadLocalMap = threadLocalsField.get(thread);
if (null != threadLocalMap) {
expungeStaleEntriesMethod.invoke(threadLocalMap);
// Verify complete cleanup - if any entry's key or value object's class is loaded by the current WAR package's classloader,
// it indicates a memory leak still exists and requires remediation.
checkThreadLocalMapForLeaks(threadLocalMap, tableField);
}
// Clear objects referenced by the inheritableThreadLocalsField
threadLocalMap = inheritableThreadLocalsField.get(thread);
if (null != threadLocalMap) {
expungeStaleEntriesMethod.invoke(threadLocalMap);
checkThreadLocalMapForLeaks(threadLocalMap, tableField);
}
}
}
} catch (Throwable t) {
// ...
}
}

The above code primarily iterates through all threads, then analyzes each thread’s ThreadLocalMap objects (including both threadLocals and inheritableThreadLocals) to detect whether thread variables have been properly cleared.

It should be noted that JDK 17 and later versions by default prohibit cross-package reflection operations. Therefore, applications need to add --add-opens=java.base/java.lang=ALL-UNNAMED to JVM parameters to lift this restriction.