Uncovering ThreadLocal Memory Leaks: A Deep Dive into Java’s ThreadLocal Implementation

This article explains how ThreadLocal works internally, demonstrates its typical use in a Spring single‑sign‑on interceptor, analyzes the source code of ThreadLocal and ThreadLocalMap, and reveals the conditions under which ThreadLocal can cause memory leaks, offering practical guidance to avoid them.

Programmer DD
Programmer DD
Programmer DD
Uncovering ThreadLocal Memory Leaks: A Deep Dive into Java’s ThreadLocal Implementation

1. Introduction

ThreadLocal is a frequently used class in frameworks such as Spring. Many articles discuss its source code but often leave the question of whether ThreadLocal can cause memory leaks unclear. This article shares a clear understanding of ThreadLocal’s implementation and its potential memory‑leak issues.

2. Use Case

One simple example is single‑sign‑on interception. Before handling an HTTP request we check whether the user is logged in, storing the login information in a ThreadLocal.

public class UserInfoHolder {
    private static final ThreadLocal<Map<String, String>> USER_INFO_THREAD_LOCAL = new ThreadLocal<>();

    public static void set(Map<String, String> map) {
        USER_INFO_THREAD_LOCAL.set(map);
    }

    public static Map<String, String> get() {
        return USER_INFO_THREAD_LOCAL.get();
    }

    public static void clear() {
        USER_INFO_THREAD_LOCAL.remove();
    }
    // ...
}

In a Spring project we can use a HandlerInterceptorAdapter to set the user info before the request and clear it after the request.

public class LoginInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<String, String> userInfoMap = getUserInfo();
        UserInfoHolder.set(userInfoMap);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        UserInfoHolder.clear();
    }
}

3. Source Code Analysis

3.1 Class Signature

ThreadLocal is a plain class without interfaces or a superclass.

public class ThreadLocal<T> { }

3.2 Constructor

ThreadLocal has only a no‑arg constructor. Since JDK 1.8 it also provides the static method withInitial that creates a ThreadLocal with a supplier.

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

3.3 ThreadLocalMap

3.3.1 Main Code

ThreadLocalMap is an inner class that stores entries in an array similar to HashMap. The hash code of a ThreadLocal is generated by adding a constant HASH_INCREMENT.

private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        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;
    // ... other fields and methods ...
}

Entry extends WeakReference<ThreadLocal<?>>, so the key is held weakly.

3.3.2 Important Points

WeakReference – the key is weakly referenced.

Addressing – the position in the array is calculated with threadLocalHashCode & (len‑1).

3.4 Core Methods

set

The set method obtains the current thread, retrieves its ThreadLocalMap, creates the map if necessary, and stores the value. If a slot is already occupied the implementation uses linear probing and may replace stale entries.

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

private ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

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

get

The get method fetches the current thread’s map, looks up the entry, and returns the stored value. If the map is absent it initializes the value via initialValue().

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();
}

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

protected T initialValue() {
    return null;
}

remove

remove

clears the entry’s key (weak reference) and expunges the stale slot.

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

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;
        }
    }
}

4. Memory‑Leak Analysis

When a thread finishes, its ThreadLocalMap becomes unreachable and the weak keys are cleared, so no leak occurs.

In a thread‑pool scenario the thread lives longer. After GC the weak key may be cleared while the value remains, causing a leak unless remove() or get() is called to clean the entry.

5. Summary

ThreadLocal provides a per‑thread copy of shared data, commonly used for storing login information.

Data is stored in ThreadLocalMap, an array of Entry objects similar to HashMap.

Improper use can lead to memory leaks; calling remove() after use prevents the leak.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaspringmemory leakThreadLocalThreadLocalMap
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.