Mastering ThreadLocal: How Java Manages Thread-Local Variables and Avoids Memory Leaks
This article explains the inner workings of Java's ThreadLocal, covering its storage mechanism, ThreadLocalMap structure, get/set/remove operations, thread isolation, potential memory leak pitfalls, and provides a practical demo program to illustrate proper usage.
Preface
After passing a Meituan interview for a Java backend engineer position, the interviewer asked about the principle of ThreadLocal. This article provides a detailed explanation of ThreadLocal's inner workings.
ThreadLocal
ThreadLocal is an internal storage class for threads, allowing data to be stored and accessed only within the specific thread.
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*/Each thread has an instance of ThreadLocalMap managed by ThreadLocal.
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;When a new thread is created, it instantiates a ThreadLocalMap and assigns it to the thread's threadLocals field. If the map already exists, it is reused.
Application Scenarios
Use ThreadLocal when data is scoped to a thread and each thread needs its own independent copy, especially in high-concurrency, stateless scenarios. It is unsuitable when business logic heavily depends on shared mutable state.
get() and set()
The set() method delegates to ThreadLocalMap.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);
}
// getMap method
ThreadLocalMap getMap(Thread t) {
// thread maintains a ThreadLocalMap
return t.threadLocals;
}
// createMap
void createMap(Thread t, T firstValue) {
// instantiate a new ThreadLocalMap and assign to threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}ThreadLocalMap
ThreadLocalMapmaintains an array table for each thread; the ThreadLocal instance determines the array index where the value is stored.
ThreadLocalMaps are lazily constructed, created only when at least one entry is placed.
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}The map initializes a default array of length 16. The index i is computed by bit‑wise AND of the ThreadLocal's hash code with the array length.
set() Method
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is
// less common to use set() to create new entries than to replace existing ones.
Entry[] tab = table;
int len = tab.length;
// compute index via &
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// if key exists, overwrite
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// insert new entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}The index is obtained by key.threadLocalHashCode & (len-1). The hash code is generated as follows:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}Because threadLocalHashCode is static, it is initialized once per class load and increments by HASH_INCREMENT each time, providing a well‑distributed hash.
get() Method
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();
}remove() Method
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;
}
}
}Thread Isolation Feature
ThreadLocal provides thread isolation: only the owning thread can retrieve its value; other threads cannot access it.
Synchronized solves conflicts by thread waiting, sacrificing time.
ThreadLocal solves conflicts by giving each thread its own storage, sacrificing space.
Memory Leak Issue
ThreadLocal can cause memory leaks if remove() is not called after use; always clean up the stored data.
Demo Program
import java.util.concurrent.atomic.AtomicInteger;
/**
* <h3>Exper1</h3>
* <p>ThreadLocalId</p>
*
* @author : cxc
* @date : 2020-04-01 23:48
*/
public class ThreadLocalId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
public static void remove() {
threadId.remove();
}
}
/**
* <h3>Exper1</h3>
* <p></p>
*
* @author : cxc
* @date : 2020-04-02 00:07
*/
public class ThreadLocalMain {
private static void incrementSameThreadId() {
try {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread() + "_" + i + ",threadId:" + ThreadLocalId.get());
}
} finally {
ThreadLocalId.remove();
}
}
public static void main(String[] args) {
incrementSameThreadId();
new Thread(new Runnable() {
@Override
public void run() {
incrementSameThreadId();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
incrementSameThreadId();
}
}).start();
}
}Conclusion
ThreadLocal is a frequently asked interview topic. Its private data is stored in ThreadLocalMap, managed by ThreadLocal. To master its principle, read the source code, especially the ThreadLocalMap implementation, and remember to clean up with remove() before the thread ends.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
