Understanding ThreadLocal in Java: Principles, Memory Leak Issues, and Proper Usage
This article explains Java's ThreadLocal mechanism, detailing its internal structure, including key methods like set, get, and remove, demonstrates how improper use can cause memory leaks, and provides best practices and examples for safe usage, including InheritableThreadLocal and real-world scenarios.
Preface
When multiple threads access shared mutable data, synchronization becomes necessary, but not every situation requires shared data, which is where ThreadLocal comes into play.
ThreadLocal, also known as thread‑local variable, allows data to be confined to each thread. A single ThreadLocal can hold a thread‑level variable that is shared among threads while still providing absolute thread safety. Its usage is shown below:
public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();RESOURCE represents a ThreadLocal that can store a String. Regardless of which thread accesses it, reads and writes are thread‑safe.
Beyond thread safety, ThreadLocal can serve as a convenient way to pass parameters without cluttering method signatures, especially when the same argument must be passed through many layers of code.
In a previous project I built a WeChat conversation archiving feature that pulled and stored dozens of message types. To avoid long parameter lists, configuration and context objects were stored in ThreadLocal, simplifying downstream code.
Later, when implementing an SMS module with multiple sending interfaces, I also used ThreadLocal to hold SMS context, template context, and other data, keeping the code clean and readable.
Now let's dive into the core of ThreadLocal.
Principle
First, look at the structure of ThreadLocal.
The key methods are:
set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}The code shows that a value is stored in the current thread's ThreadLocalMap using the ThreadLocal instance as the key.
ThreadLocalMap is a static inner class of ThreadLocal that manages multiple ThreadLocal instances for a thread. It is essentially a simple map backed by an array, with an initial capacity of 16 and a resize threshold of two‑thirds of the array size. Collisions are resolved by linear probing.
static class ThreadLocalMap {
/**
* Entry class storing key‑value pairs
*/
static class Entry extends WeakReference
> {
// The value associated with the current thread; not weakly referenced
Object value;
Entry(ThreadLocal
k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold;
}Each thread holds a ThreadLocalMap instance:
/* ThreadLocal values pertaining to this thread. This map is maintained
by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;Thus, setting a value actually writes into the current thread's map, ensuring isolation between threads.
get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}Retrieving a value looks up the current thread's ThreadLocalMap using the ThreadLocal instance as the key, guaranteeing thread‑local visibility.
remove
private void remove(ThreadLocal
key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}Calling remove clears the entry, preventing memory leaks that can occur when a ThreadLocal is garbage‑collected but its value remains referenced by the thread.
ThreadLocal Memory Leak and Correct Usage
Because the key in ThreadLocalMap is a weak reference, when the ThreadLocal object itself is reclaimed, the key becomes null while the value stays strongly referenced, leading to a leak as long as the thread lives.
The following test demonstrates this behavior. After invoking System.gc() , the key becomes null while the value persists:
@Test
public void loop() throws Exception {
for (int i = 0; i < 1; i++) {
ThreadLocal
threadLocal = new ThreadLocal<>();
threadLocal.set(new SysUser(System.currentTimeMillis(), "李四"));
// threadLocal = null;
// System.gc();
printEntryInfo();
}
}
private void printEntryInfo() throws Exception {
Thread currentThread = Thread.currentThread();
Class
clz = currentThread.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(currentThread);
Class
tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class
entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
}To avoid this leak, keep a strong reference to the ThreadLocal (e.g., declare it as a static final constant) and always call remove() when the thread finishes its work.
private static final ThreadLocal
RESOURCE = new ThreadLocal<>();
@Test
public void multiThread() {
Thread thread1 = new Thread(() -> {
RESOURCE.set("thread1");
System.gc();
try { printEntryInfo(); } catch (Exception e) { e.printStackTrace(); }
});
Thread thread2 = new Thread(() -> {
RESOURCE.set("thread2");
System.gc();
try { printEntryInfo(); } catch (Exception e) { e.printStackTrace(); }
});
thread1.start();
thread2.start();
}Even after garbage collection, the key remains the same because the static constant is never reclaimed.
弱引用key:java.lang.ThreadLocal@10c6d8a7,值:thread1
弱引用key:java.lang.ThreadLocal@10c6d8a7,值:thread2However, merely using a global constant is not enough; you must also call remove() after each request to prevent stale data from contaminating reused threads in a thread pool.
package com.cube.share.thread.config;
import com.cube.share.thread.entity.SysUser;
public class CurrentUser {
private static final ThreadLocal
USER = new ThreadLocal<>();
private static final ThreadLocal
USER_ID = new ThreadLocal<>();
private static final InheritableThreadLocal
INHERITABLE_USER = new InheritableThreadLocal<>();
private static final InheritableThreadLocal
INHERITABLE_USER_ID = new InheritableThreadLocal<>();
public static void setUser(SysUser sysUser) { USER.set(sysUser); INHERITABLE_USER.set(sysUser); }
public static void setUserId(Long id) { USER_ID.set(id); INHERITABLE_USER_ID.set(id); }
public static SysUser user() { return USER.get(); }
public static SysUser inheritableUser() { return INHERITABLE_USER.get(); }
public static Long userId() { return USER_ID.get(); }
public static Long inheritableUserId() { return INHERITABLE_USER_ID.get(); }
public static void removeAll() { USER.remove(); USER_ID.remove(); INHERITABLE_USER.remove(); INHERITABLE_USER_ID.remove(); }
}Integrate this cleanup in a servlet request listener or an AOP aspect to clear ThreadLocal data after each request.
@Component
@Slf4j
public class ServletRequestHandledEventListener implements ApplicationListener
{
@Override
public void onApplicationEvent(ServletRequestHandledEvent event) {
CurrentUser.removeAll();
log.debug("Cleared thread user info, uri = {}, method = {}, servletName = {}, clientAddress = {}",
event.getRequestUrl(), event.getMethod(), event.getServletName(), event.getClientAddress());
}
}InheritableThreadLocal for Child Threads
If a child thread needs to inherit the parent thread's ThreadLocal values, use InheritableThreadLocal . The JDK copies the parent’s inheritableThreadLocals map when the child thread is created.
// Holds a map that can be inherited by child threads
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ... other initialization ...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// ...
}Use Cases
Thread‑level data isolation: each thread’s ThreadLocal does not affect others.
Convenient parameter passing within the same thread, avoiding bulky method signatures.
Propagation of trace IDs in distributed tracing or workflow engines.
Spring transaction management relies on ThreadLocal.
Spring MVC’s RequestContextHolder implementation uses ThreadLocal.
Conclusion
The article dissected ThreadLocal from source code, explained how memory leaks occur, and presented correct usage patterns. It also mentioned variants such as FastThreadLocal (better performance) and TransmittableThreadLocal (solves thread‑pool inheritance issues).
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.