ThreadLocal set(T) Method Behavior
Before diving deep into ThreadLocalMap, let's clarify a common misconception: calling set() multiple times on the same ThreadLocal instance within a thread's execution only stores the last value. Examining the source code reveals why:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
When a ThreadLocalMap already exists for the current thread, the set() method directly invokes ThreadLocalMap's set method. Since the same ThreadLocal object (identified by this) always computes the same array index via the hash code formula, subsequent calls overwrite the previous value at that position.
When the ThreadLocalMap doesn't exist, createMap() instantiates a new one, wrapping the ThreadLocal object and value into an Entry and storing it in the internal table array.
This design becomes particularly useful when different classes need to store thread-local data. For instance, when Thread A's run() method calls method job() in class B, which in turn calls doJob() in independent class C, each class can define its own ThreadLocal and store data directly without coordinating through shared structures. Defining the thread's storage as a map-like structure enables this convenient pattern.
ThreadLocalMap Deep Dive
ThreadLocalMap is a static inner class within the ThreadLocal class that implements thread-local storage (TLS). Each thread maintains its own ThreadLocalMap instance where the keys are ThreadLocal objects and the values are the variables we want to isolate per thread.
When calling ThreadLocal's get() or set() methods, operations actually target the thread's associated ThreadLocalMap. This guarantees that each thread accesses only its own variable copies, preventing data sharing issues in multithreaded environments.
A important implementation detail: ThreadLocalMap uses weak references (WeakReference) for storing ThreadLocal instances as keys. This means if no strong references to a ThreadLocal exist elsewhere, the garbage collector can reclaim it, potentially reclaiming associated values to prevent memory leaks.
set(T) Operation Details
The ThreadLocalMap constructor performs straightforward operations:
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 constructor creates the entry table array, calculates the storage position using hash-based indexing, creates an Entry wrapper, and initializes the size and threshold. The INITIAL_CAPACITY constant equals 16.
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
The Entry class is a static inner class extending WeakReference, with its referent being the ThreadLocal object. It also contains an Object value field holding the stored thread-local data.
Now examining the set() method implementation:
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)]) {
if (e.refersTo(key)) {
e.value = value;
return;
}
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
The logic copies the table reference and calculates the target index using hash-based modulo. The loop traverses entries: if a matching key is found, the value is updated; if an entry with a null key (garbage-collected) is encountered, replaceStaleEntry handles it. Otherwise, a new Entry is inserted at the first available position.
The cleanSomeSlots() method removes entries with garbage-collected keys. If no cleanup occurs and size reaches the threshold, rehash() triggers expansion.
public class ThreadLocalExpansionDemo {
static ThreadLocal<String> loc1 = new ThreadLocal<>();
static ThreadLocal<String> loc2 = new ThreadLocal<>();
static ThreadLocal<String> loc3 = new ThreadLocal<>();
static ThreadLocal<String> loc4 = new ThreadLocal<>();
static ThreadLocal<String> loc5 = new ThreadLocal<>();
static ThreadLocal<String> loc6 = new ThreadLocal<>();
static ThreadLocal<String> loc7 = new ThreadLocal<>();
static ThreadLocal<String> loc8 = new ThreadLocal<>();
static ThreadLocal<String> loc9 = new ThreadLocal<>();
static ThreadLocal<String> loc10 = new ThreadLocal<>();
static ThreadLocal<String> loc11 = new ThreadLocal<>();
static ThreadLocal<String> loc12 = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread() {
@Override
public void run() {
loc1.set("Worker-0");
loc2.set("Worker-1");
loc3.set("Worker-2");
loc4.set("Worker-3");
loc5.set("Worker-4");
loc6.set("Worker-5");
loc7.set("Worker-6");
loc8.set("Worker-7");
loc9.set("Worker-8");
loc10.set("Worker-9");
loc11.set("Worker-10");
loc12.set("Worker-11");
}
};
worker.start();
worker.join();
}
}
When entries exceed the threshold (default 10) and no garbage collection has occurred, the table undergoes expansion from 16 to 32 slots.
| Data | Pre-expansion Index | Post-expansion Index |
|---|---|---|
| Worker-0 | 4 | 20 |
| Worker-1 | 11 | 27 |
| Worker-2 | 2 | 2 |
| Worker-3 | 9 | 9 |
| Worker-4 | 0 | 16 |
| Worker-5 | 7 | 23 |
| Worker-6 | 14 | 30 |
| Worker-7 | 5 | 5 |
| Worker-8 | 12 | 12 |
| Worker-9 | 3 | 19 |
Resizing Mechenism
ThreadLocalMap expands dynamically based on resource usage. The rehash() method handles capacity increase:
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
The expungeStaleEntries() method scans the entire table and removes entries with null keys (garbage-collected ThreadLocals):
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.refersTo(null))
expungeStaleEntry(j);
}
}
The resize() method doubles the table capacity:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (Entry e : oldTab) {
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
This operation backs up the old table, creates a new array twice the size, recalculates indices for all valid entries using the new capacity, and updates the table reference.
Hash Collision Resolution
Hash collision occurs when different keys produce the same hash index. With finite hash buckets for theoretically infinite inputs, collision becomes inevitable.
Common collision resolution strategies include:
-
Open Addressing: Linear probing checks consecutive slots; quadratic probing uses square sequences; double hashing applies a secondary hash function.
-
Chaining: HashMap in Java uses linked lists at each bucket position. New elements append to the chain.
-
Rehashing: Secondary hash function are applied until finding an empty slot.
-
Separate Overflow Area: Conflicts redirect to a dedicated overflow region.
-
Bloom Filters: Probabilistic structures for membership testing.
ThreadLocalMap employs open addressing with linear probing:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
When collision occurs, the algorithm searches sequentially from the computed index, wrapping around to index 0 when reaching the array boundary. This linear probing approach matches the described open addressing strategy.
Java Reference Types
Java provides four reference types with different garbage collection behaviors:
-
Strong Reference: Default reference type. Objects with strong references are never garbage collected until all references are cleared.
Object strongRef = new Object(); -
Soft Reference: Garbage collected when memory is insufficient, but before throwing OutOfMemoryError. Suitable for memory-sensitive caching.
SoftReference<Object> softRef = new SoftReference<>(new Object()); -
Weak Reference: Collected on next GC cycle regardless of memory availability. Commonly used to prevent memory leaks in "orphaned" data structures.
WeakReference<Object> weakRef = new WeakReference<>(new Object()); -
Phantom Reference: The weakest reference type. Cannot retrieve the object and doesn't affect collection. Primarily used for receiving notifications before object reclamation.
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
Memory Leak Prevention in ThreadLocal
Memory leak occurs when allocated memory cannot be reclaimed, progressively consuming available system memory. Common causes include:
- Forgotten deallocation in manual memory management
- Dangling pointers referencing freed memory
- Circular references in garbage-collected languages
- Long-lived objects holding references to unnecessary objects
ThreadLocal can exhibit memory leaks when thread pools reuse threads:
public class ThreadLocalLeakExample {
private static final ThreadLocal<byte[]> CACHE = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
executor.execute(() -> {
byte[] buffer = new byte[10240 * 5024];
CACHE.set(buffer);
// Forgetting remove() causes memory leak
});
}
executor.shutdown();
executor.awaitTermination(2, TimeUnit.MINUTES);
}
}
The Entry class uses weak references for keys, allowing garbage collection of ThreadLocal objects when no external references exist. However, the corresponding values remain strongly referenced until the Entry itself is cleared, which occurs during table expansion or explicit removal.
Best practices include calling remove() after use, especially in long-running applications with thread pools:
try {
threadLocal.set(importantData);
// business logic
} finally {
threadLocal.remove();
}
Key Implementation Details
ThreadLocalMap shares similarities with HashMap but doesn't implement the Map interface. Its underlying structure is an Entry[] array where each Entry uses a weak reference to the ThreadLocal as key and an Object as value.
The hash calculation uses a special constant 0x61c88647, which is a Fibonacci golden ratio number. This ensures exceptionally uniform distribution of ThreadLocal objects across the hash table, minimizing clustering and collision density.