What is ThreadLocal?
ThreadLocal provides thread-local variables, where each thread accessing a variable (via get or set) has its own independently initialized copy. ThreadLocal instances are typically private static fields that associate state with a thread.
ThreadLocal enables thread isolation—each thread gets its own variable副本, preventing data races with out synchronization.
Basic Operations
private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<>();
// Set value for current thread
LOCAL_FOO.set(new Foo());
// Get value for current thread
Foo foo = LOCAL_FOO.get();
// Remove value (critical to prevent leaks)
LOCAL_FOO.remove();
Common Methods
| Method | Description |
|---|---|
set(T value) |
Binds a value to the current thread's local variable |
T get() |
Retrieves the value bound to the current thread |
remove() |
Removes the value from the current thread |
initialValue() |
Returns initial value when no value is set (defaults to null) |
ThreadLocal Internal Mechanism
ThreadLocalMap Structure
In JDK 8, each Thread has an internal ThreadLocalMap. Unlike early JDK versions where ThreadLocal held the Map, now the Thread owns the Map. Multiple ThreadLocal instances can coexist in one thread's ThreadLocalMap.
public class Thread {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
class ThreadLocal<T> {
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
Key Insight
ThreadLocal itself doesn't store values—it acts as a key. Values are stored in the Thread's ThreadLocalMap, with ThreadLocal instances as keys.
How set() Works
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
Memory Leak Causes and Solutions
The Memory Leak Problem
ThreadLocalMap entries have:
- Key: ThreadLocal (weak reference) → can be GC'd
- Value: Object (only has strong reference from ThreadLocalMap)
Memory leak scenario:
- ThreadLocal is set by a thread
- ThreadLocal reference becomes null (local variable goes out of scope)
- Key gets GC'd (weak reference), but Value remains because ThreadLocalMap holds a strong reference
- If the thread is long-lived (e.g., in a thread pool), the Entry with stale Value persists indefinitely
Why Entry Uses WeakReference for Keys
Without weak references, the ThreadLocal object would never be garbage collected while the thread lives, causing a permanent leak.
With weak references, the ThreadLocal key can be collected. However, this only solves the key leak—the value still leaks because the Entry (with null key but valid value) remains in the array.
Cleanup Mechanisms
1. Proactive Cleanup (expungeStaleEntry)
Called during get(), set(), and remove() operations. Scans the map and removes entries with null keys.
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash remaining entries
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null) h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
2. Heuristic Cleanup (cleanSomeSlots)
Called during set() when size exceeds threshold. Checks a portion of the array for stale entries.
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ((n >>>= 1) != 0);
return removed;
}
Best Practice: Manual Removal
Automatic cleanup only triggers during ThreadLocal operations. In long-running applications, always call remove() explicitly:
try {
// Use ThreadLocal
} finally {
LOCAL_FOO.remove();
}
ThreadPool Consideration
With thread pools, threads are reused. If ThreadLocal values aren't cleaned, they acccumulate. For thread pools, consider clearing in afterExecute():
ThreadPoolExecutor executor = new ThreadPoolExecutor(...) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
START_TIME.remove();
}
};
InheritableThreadLocal (ITL)
InheritableThreadLocal extends ThreadLocal to support parent-to-child value inheritance. When a child thread is created, it inherits parent's InheritableThreadLocal values.
private static final InheritableThreadLocal<Integer> traceId =
new InheritableThreadLocal<>();
How Inheritance Works
The Thread.init() method copies parent's ThreadLocalMap to child:
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
Limitations
- Inheritance only works for
new Thread()orThread(Runnable) - Does not work with thread pools (new tasks may run on different pooled threads)
- Values are copied at thread creation time, not updated dynamically
TransmittableThreadLocal (TTL)
TransmittableThreadLocal, developed by Alibaba, solves the thread pool inheritance problem by providing explicit value capture and replay mechanisms.
Core Concept
TTL uses TtlRunnable or TtlCallable wrappers to:
- Capture ThreadLocal values when task is submitted
- Replay those values when task executes
// Wrap runnable to capture and replay ThreadLocal values
executor.execute(TtlRunnable.get(task));
Mechanism
public final class TtlRunnable implements Runnable {
private final AtomicReference<Map<ThreadLocal<?>, Object>> capturedRef;
@Override
public void run() {
Map<ThreadLocal<?>, Object> captured = capturedRef.get();
for (Map.Entry<ThreadLocal<?>, Object> entry : captured.entrySet()) {
((ThreadLocal<?>) entry.getKey()).set(entry.getValue());
}
try {
runnable.run();
} finally {
// Clean up captured values
}
}
}
Usage
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
context.set("main-value");
// With executor
TtlExecutorService executor = TtlExecutors.getTtlExecutorService(executorService);
executor.submit(() -> System.out.println(context.get())); // Prints "main-value"
FastThreadLocal (FTL)
Netty's FastThreadLocal provides O(1) access instead of the O(n) linear probing used by standard ThreadLocalMap.
How It Works
Each FastThreadLocal gets a unique index at creation:
public class FastThreadLocal<T> {
private final int index;
public FastThreadLocal() {
this.index = InternalThreadLocalMap.nextVariableIndex();
}
}
Values are stored in an InternalThreadLocalMap object array:
- Index 0: Set of all FastThreadLocals
- Index N: Value for FastThreadLocal with that index
public final class InternalThreadLocalMap {
private Object[] indexedVariables = new Object[32];
public Object get(FastThreadLocal<?> key) {
int index = key.index;
return indexedVariables[index];
}
public void set(FastThreadLocal<?> key, Object value) {
expandIndexedVariablesIfNeeded(index);
indexedVariables[index] = value;
}
}
Access Pattern
Direct array index access is O(1), avoiding hash collisions entirely.
Requires FastThreadLocalThread
FastThreadLocal only provides full benefits when used with FastThreadLocalThread:
FastThreadLocalThread thread = new FastThreadLocalThread(() -> {
FastThreadLocal<String> local = new FastThreadLocal<>();
local.set("value");
System.out.println(local.get()); // O(1) access
});
thread.start();
Comparison
| Aspect | ThreadLocal | InheritableThreadLocal | TransmittableThreadLocal | FastThreadLocal |
|---|---|---|---|---|
| Scope | Single thread | Parent to child thread | Thread pool inheritance | Single thread |
| Thread pool support | No | No | Yes | Yes |
| Access time | O(n) | O(n) | O(n) | O(1) |
| Memory leak protection | Cleanup on access | Cleanup on access | Cleanup on access | Better (Set-based) |
| Use case | Simple thread isolation | Parent-child propagation | Task submission to pools | High-performence Netty |
Performance Considerations
ThreadLocal's hash-based storage uses linear probing for collision resolution, which degrades to O(n) with many entries. FastThreadLocal avoids this by using fixed indices.
For high-throughput applications:
- Static final ThreadLocal instances reduce memory footprint
- Always remove() values in long-running threads
- Consider FastThreadLocal for performance-critical paths
- TTL adds overhead but solves pool inheritance problems