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.

Programmer DD
Programmer DD
Programmer DD
Mastering ThreadLocal: How Java Manages Thread-Local Variables and Avoids Memory Leaks

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

ThreadLocalMap

maintains an array table for each thread; the ThreadLocal instance determines the array index where the value is stored.

ThreadLocalMap illustration
ThreadLocalMap illustration
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.

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.

JavaBackend Developmentmemory leakThreadLocalthread isolationThreadLocalMap
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.