Understanding ThreadLocal: Thread Isolation in Java

Thread isolation mechanism in Java.

ThreadLocal provides a way to isolate variable access per thread, ensuring thread-safe manipulation of shared data in a multi-threaded environment.

static ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
  protected Integer initialValue() {
    return 0;
  }
};

public static void main(String[] args) {
  Thread[] threads = new Thread[5];
  for (int i = 0; i < 5; i++) {
    threads[i] = new Thread(() -> {
      int val = counter.get();
      counter.set(val + 5);
      System.out.println(Thread.currentThread().getName() + "-" + val);
    });
  }
  for (int i = 0; i < 5; i++) {
    threads[i].start();
  }
}

How ThreadLocal Works Internally

The set() Method

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

On the first invocation when the map doesn't exist, a ThreadLocalMap is created lazily.

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

When the map already exists, the following steps occur:

  • Compute the array index for the Entry using hash-based slot calculation
  • Use linear probing to traverse from index i to the end of the array
  • If the map contains the same key, the existing value gets overwritten
  • If the key in the map is null, replace it with the new key-value pair and clean up stale entries
  • Trigger rehash扩容 when threshold is exceeded
private void set(ThreadLocal<?> key, Object value) {
  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)]) {
    ThreadLocal<?> k = e.get();

    if (k == key) {
      e.value = value;
      return;
    }

    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }

  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

Linear Probing is an open addressing strategy used to resolve hash collitions. A hash table maps keys to array positions via a hash function for fast data access. When two keys hash to the same position, linear probing handles the conflict:

  • Insertion: Find the nearest empty slot after the collision point and place the new key-value pair there
  • Lookup: Starting from the hashed position, search forward until finding the matching key or an empty slot

replaceStaleEntry

This method handles stale entry cleanup and replacement.

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;
  Entry e;

  int slotToExpunge = staleSlot;
  for (int i = prevIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = prevIndex(i, len))
    if (e.get() == null)
      slotToExpunge = i;

  for (int i = nextIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();

    if (k == key) {
      e.value = value;
      tab[i] = tab[staleSlot];
      tab[staleSlot] = e;

      if (slotToExpunge == staleSlot)
        slotToExpunge = i;
      cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
      return;
    }

    if (k == null && slotToExpunge == staleSlot)
      slotToExpunge = i;
  }

  tab[staleSlot].value = null;
  tab[staleSlot] = new Entry(key, value);

  if (slotToExpunge != staleSlot)
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

0x61c88647: The Fibonacci Hash Constant

private static final int HASH_INCREMENT = 0x61c88647;

public static void main(String[] args) {
  magicHash(16);
  magicHash(32);
}

private static void magicHash(int size) {
  int hashCode = 0;
  for (int i = 0; i < size; i++) {
    hashCode = i * HASH_INCREMENT + HASH_INCREMENT;
    System.out.print((hashCode & (size - 1)) + " ");
  }
  System.out.println();
}

Output:

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26
1 8 15 22 29 4 11 18 25 0

This magic number produces a distributed sequence that minimizes clustering in the hash table.

Why ThreadLocal Uses Weak References

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }
}
  • A weak reference allows garbage collection even when only weakly referenced. When the next GC runs and an object has only weak references pointing to it, that object gets collected
  • ThreadLocalMap's Entry keys are weak references. When a key becomes null, both get() and set() operations will clean up these stale entries
  • If keys were strong references, setting threadLocal = null would still leave thread -> threadLocalMap -> entry -> key (threadLocal). This would prevent the Entry value from being garbage clolected until the ThreadLocal instance is no longer in use, causing a memory leak
  • With thread pools, if values aren't menually cleared, they persist in entries indefinitely, leading to memory leaks

Bitwise AND Operator Reference

The & operator performs bitwise AND on binary representations. Both bits must be 1 for the result to be 1.

Example: 132 & 15 = ?

  • 132 in binary: 10000100
  • 15 in binary: 00001111
  • Result: 10000100 & 00001111 = 00000100 = 4

Rules:

  • When bits differ (1 and 0), the result is 0
  • When bits match (both 1), the smaller value wins in the final result

Tags: java ThreadLocal Concurrency Thread Safety Memory Management

Posted on Thu, 25 Jun 2026 16:19:40 +0000 by mashnote