preface

ThreadLocal is used to store and retrieve local variables from each thread in a multithreaded environment. These local variables are bound to threads and do not affect each other. In this article, you’ll learn how to use ThreadLocal and how it works.

The body of the

Use of ThreadLocal

Take a simple example to illustrate the use of ThreadLocal.

Typically, ThreadLocal is used by declaring it as a private static field of the class, as shown below.

public class ThreadLocalLearn { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public void setThreadName(String threadName) { threadLocal.set(threadName); } public String getThreadName() { return threadLocal.get(); }}

The ThreadLocalLearn class has a private static ThreadLocal object field. Each thread holds its name through the setThreadName() method provided by ThreadLocalLearn. Get the thread name through the getThreadName() method.

Write a test program as shown below.

class ThreadLocalLearnTest {

    private ThreadLocalLearn threadLocalLearn;

    @BeforeEach
    public void setUp() {
        threadLocalLearn = new ThreadLocalLearn();
    }

    @Test
    void givenMultiThreads_whenSetThreadNameToThreadLocal_thenCanGetThreadNameFromThreadLocal() {
        Thread threadApple = new Thread(() -> {
            threadLocalLearn.setThreadName("Thread-Apple");
            System.out.println(Thread.currentThread().getName() + ": " + threadLocalLearn.getThreadName());
        }, "Thread-Apple");

        Thread threadPeach = new Thread(() -> {
            threadLocalLearn.setThreadName("Thread-Peach");
            System.out.println(Thread.currentThread().getName() + ": " + threadLocalLearn.getThreadName());
        }, "Thread-Peach");

        threadApple.start();
        threadPeach.start();
    }

}

Two threads were enabled in the test program, and they performed the same operation, storing and then fetching the name of the thread through the written ThreadLocalLearn. The printed result is shown below.

Thread-Apple: Thread-Apple
Thread-Peach: Thread-Peach

The result of the print shows that the local variables of the two threads are bound to the threads, and the threads do not affect each other.

How ThreadLocal works.

Let’s start by examining the ThreadLocal’s set() method, whose source is shown below.

Public void set(T value) {// Get Thread T = Thread.currentThread(); ThreadLocalMap (threadLocalMap) = getMap(t); if (map ! ThreadLocalMap (ThreadLocalMap, ThreadLocalMap, ThreadLocalMap, ThreadLocalMap, ThreadLocalMap); CreateMap (t, value); CreateMap (t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

The set() method of a ThreadLocal stores its value in the ThreadLocalMap of the current thread. Each thread object has a field called ThreadLocals, which is an object of type ThreadLocalMap. The ThreadLocalMap class is a static inner class of the ThreadLocal class that is used by thread objects to store a thread-exclusive copy of a variable.

How ThreadLocalMap stores a thread-exclusive copy of a variable will be examined in a later section, as it is not actually a Map. Let’s take a look at ThreadLocal’s get() method.

Public T get() {public T get() {public T = Thread.currentThread(); ThreadLocalMap (threadLocalMap) = getMap(t); if (map ! GetEntry (this) {// Get the value ThreadLocalMap from the current thread's ThreadLocalMap using the ThreadLocal object as the key; if (e ! = null) { T result = (T)e.value; return result; } // If there is no ThreadLocalMap in the current thread, create a ThreadLocalMap and store an initial value in the ThreadLocalMap using the ThreadLocal object as the key. Return setInitialValue(); return setInitialValue(); 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; }

The ThreadLocal uses its own object as the key to retrieve the value from the ThreadLocalMap of the current thread.

Through the analysis ofThreadLocaltheset()andget()The method shows that,ThreadLocalThe ability to store and obtain thread local variables in a multi-threaded environment is essentially to store the value of local variables in each thread objectThreadLocalMap, so threads do not influence each other.

The principle of ThreadLocalMap

ThreadLocalMap is not itself a Map, but it is possible to store local variables in the thread as key-values. Similar to Map, ThreadLocalMap encapsulates the key-value relationship in an Entry object, which is the static inner class of ThreadLocalMap. The source code is shown below.

static class Entry extends WeakReference<ThreadLocal<? >> { Object value; Entry(ThreadLocal<? > k, Object v) { super(k); value = v; }}

Entry inherits from a WeakReference, so Entry is a weakly referenced object, while the ThreadLocal object as a key is a weakly referenced object.

We first analyze the constructor of ThreadLocalMap. ThreadLocalMap has two constructors. Here we only analyze the signature ThreadLocalMap(ThreadLocal
FirstKey, Object FirstValue).

ThreadLocalMap(ThreadLocal<? > firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY]; / / hash algorithm is used to calculate the index of the first key values in the array of int I = firstKey. ThreadLocalHashCode & (INITIAL_CAPACITY - 1); Table [I] = new Entry(firstKey, firstValue); size = 1; // setThreshold(INITIAL_CAPACITY); // setThreshold(INITIAL_CAPACITY); }

The hash algorithm of ThreadLocalMap is to subtract the length of the Entry array from the hash code of ThreadLocal. Since the length of the Entry array is a power of two, the essence of the appeal hash algorithm is that the hash code of ThreadLocal modulo the length of the Entry array. After the initial key-value pair’s position in the Entry array is calculated by the hash algorithm, an Entry object is created and placed in the corresponding position in the array. Finally, the expansion threshold is calculated according to the formula Len * 2/3.

From the above analysis, we can see that when we create a ThreadLocalMap object, we initialize the Entry array that holds the key-value pair relationships. Now look at the set() method of ThreadLocalMap.

Private void set(ThreadLocal<?); private void set(ThreadLocal<?); > key, Object value) { Entry[] tab = table; int len = tab.length; Int I = key.threadLocalHashCode & (len-1); int I = key.threadLocalHashCode & (len-1); // For (Entry e = TAB [I]; e ! = null; E = Tab [I = nextIndex(I, len)]) { > k = e.get(); If (k == key) {e.value = value; if (k == key) {e.value = value; if (k == key) {e.value = value; return; If (k == null) {replaceStaleEntry(key, value, I);} if (k == null) {replaceStaleEntry(key, value, I); return; }} // When traversing through the Entry array, the key of the traversing through the Entry array is not the same as the key of the key-value pair until the first empty slot is found. TAB [I] = new Entry(key, value); TAB [I] = new Entry(key, value); int sz = ++size; if (! CleansomesLots (I, sz) &&sz >= threshold) // When the number of key-value pairs in the Entry array is greater than or equal to the threshold value, the cleanSomeSlots(I, sz) &&sz >= threshold value If the number of key pairs is greater than or equal to 3/4 of the threshold value, then the size of the Entry array should be twice as large as before; } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }

In the set() method, the index position of the key-value pair is first computed using the hash algorithm, and then the Entry array is traversed from the computed index position until the first empty slot is traversed. During traversal, if the key of the traversal to an Entry is equal to the key of the key-value pair, update the value of the Entry to the key-value pair. If an Entry is traversal and the Entry is judged stale (an Entry whose key was garbage-collected), then the logic to clear the stale Entry is performed; If the traversal encounters an empty slot, but does not find an Entry whose key is equal to the key of the key-value pair, and there are no stale entries, then an Entry object is generated from the key-value pair and placed in the empty slot location.

In the set() method, when you need to clear stale items, you call the replaceStaleEntry() method, which creates the Entry object based on the key-value pair and replaces the stale item, triggering the logic to clear the stale item once. The implementation of the replaceStaleEntry() method is shown below.

// Table [staleSlot] is stale // This method essentially traversal the Entry array from staleSlot until it hits an empty slot. If the key of an Entry is found to be equal to the key of the key-value pair, then the value of the Entry is updated to the value of the key-value pair. If no Entry with a key equal to the key of the key-value pair is found, then clear the stale Entry directly. Private void replaceStaleEntry(ThreadLocal<??) 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); 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) {if (k == key) {if (k == key) {e.value = value; // Swap the updated Entry with the old Entry whose index is staleSlot TAB [I] = TAB [staleSlot]; tab[staleSlot] = e; If (slotToExpunge == staleSlot) slotToExpunge = I; if (slotToExpunge == staleSlot) // ExpungeStaleEntry (int I) clears all stale entries from the slot in I to the next empty slot // CleansomesLots (int I, int n) clears log2 slots from the slot in I back. If stale items are found, clear them and scan backward for log2(table.length) slots CleansomesLots (expungestaleEntry (slotToExpunge), Len); return; } // If an Entry is stale and no stale entries are found when trawling through the Entry array, If (k == null &&slotToExpunge == staleSlot) slotToExpunge = I; } // When traversal the Entry array backwards from the slot with staleSlot index until an empty slot is encountered and no key is found equal to the key of the staleSlot pair, the stale entries in the staleSlot position are cleared directly. Create an Entry object with a staleSlot index TAB [staleSlot]. Value = null; tab[staleSlot] = new Entry(key, value); / / in the beginning, staleSlot and slotToExpunge are equal, once staleSlot do not equal with slotToExpunge, indicates that from staleSlot position forward or backward traversal Entry arrays, StaleSlot location in addition to StaleSlot location in addition to StaleSlot location = staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }

Two key methods are called in replaceStaleEntry(), expungeStaleEntry(int I) to clear stale entries from the I slot and all stale entries from the slot in I to the next empty slot; CleansomesLots (int I, int n) can be used to scan back log2(n) slots from position I. If stale items are found, the CleansomesLots can be used to scan back log2(tab.length) slots. The implementation is as follows.

private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // delete staleSlot from staleSlot TAB [staleSlot]. Value = null; tab[staleSlot] = null; size--; // If you iterate to a non-stale Entry, then re-hash the Entry to the indexed position Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) ! = null; i = nextIndex(i, len)) { ThreadLocal<? > k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h ! = i) { tab[i] = null; while (tab[h] ! = null) h = nextIndex(h, len); tab[h] = e; }} return I;} return I; } private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e ! = null &&equet () == null) {// Once an Entry is scanned, reset the length of the Entry array to the next empty slot, and then clear the scanned Entry to the next empty slot. Finally, scan log2(table.length) slots back from the empty slot position n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) ! = 0); return removed; }

Since there are so many cases in the replacestaleEntry () method, it’s not straightforward to see how ThreadLocalMap clears stale items from code alone, so let’s look at the diagram. The default Entry array is 16.

Scene 1:EntryThe array slot distribution is shown below.

StaleSlot forward sets the SlotToExpunge value to 2. StaleSlot backward updates the value of an Entry with index 6 because the key is the same as the key of the key-value pair. And swap places with the obsolete item in the Staleslot position (index 4). After switching positions, the slot distribution of the Entry array is shown below.

So the logic to clear out the stale item is triggered at the end. SlotToExpuge (SlotToExpuge) = SlotToExpuge (SlotToExpuge) = SlotToExpuge (SlotToExpuge) = SlotToExpuge (SlotToExpuge) = SlotToExpuge (SlotToExpuge); Then from the next slot of the empty slot, scan log2(16) = 4 slots backwards, that is, scan the slots with index of 8,9,10,11 successively, and find stale items when scanning to the slot with index of 10. At this time, clear all stale items between the slot of index 10 and the next empty slot, that is, the stale items of the slot of index 10 will be cleared. Then scan log2(16) = 4 slots from the next slot of the empty slot backwards, that is, scan the slots with index of 13,14,15,0 in turn, and no stale items are found. The scan ends and returns true, indicating that stale items have been scanned and cleared.

Scene 2:EntryThe array slot distribution is shown below.

From staleSlot forward traversal, until it encounters an empty slot, there is no obsolete item, so at the end of the forward traversal, slotToExpunge is equal to staleSlot. When traverse backward to the slot of index 5, stale entries are found. Since slotToExpunge is equal to staleSlot at this point, set slotToExpunge to 5. Continuing to traversal backwards, since the key of the Entry object with index 6 is equal to the key of the key-value pair, the value of this Entry object is updated and replaced with the stale Entry of the StaleSlot position (index 4). After switching positions, the slot distribution of the Entry array is shown below.

As a result, a cleanup logic is finally triggered, which is the same as Scenario 1 and will not be repeated here.

Scenario 3:EntryThe array slot distribution is shown below.

From staleSlot traversed forward, slotToExpunge value will be set to 2, from staleSlot traversed back, until the empty slot, also found no key and key/value pair key equal Entry, will therefore index for staleSlot slot obsolete items directly to clear, Create an Entry object based on the key-value pair and store it at a position with an index of StaleSlot. The distribution of Staleslot slots after the stale items of Staleslot slots are cleared is shown below.

The logic for removing stale items later is the same as in Scenario 1 and will not be repeated here.

The above slot distribution might not occur in a real scenario, but as an example, the execution flow of the replaceStaleEntry() method is illustrated.

Let’s look at the getEntry() method again.

private Entry getEntry(ThreadLocal<? Int I = key.threadLocalHashCode & (table.length - 1); int I = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e ! = null &&equet () == key) // If the key of an Entry is equal to key in the index position of an Entry array, return the Entry; Else // execute getEntryAfterMiss() method if no Entry for key is found return getEntryAfterMiss(key, I, e); } private Entry getEntryAfterMiss(ThreadLocal<?);} private Entry getEntryAfterMiss(ThreadLocal<?); > key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e ! = null) { ThreadLocal<? > k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }

In either the set() or getEntry() methods, when stale items are found, the logic to clear the Entry array of stale items is triggered, which is ThreadLocal’s protection against memory leaks.

Why does ThreadLocal have memory leaks? Given that each thread has a ThreadLocalMap field, ThreadLocalMap encapsulates the key-value relationship as an Entry object, which is the static inner class of ThreadLocalMap and is implemented as follows.

static class Entry extends WeakReference<ThreadLocal<? >> { Object value; Entry(ThreadLocal<? > k, Object v) { super(k); value = v; }}

So Entry is a weakly referenced object, the ThreadLocal as a key is a weakly referenced object, and the ThreadLocal as a value is a strongly referenced object. If there are no other references to the key ThreadLocal, then the key ThreadLocal will be reclaimed on the next garbage collection. At this point the Entry becomes a stale Entry, and if the stale Entry is not cleared, the value of the stale Entry will never be reclaimed, resulting in a memory leak. If a thread’s ThreadLocalMap has a large number of stale items, the stack will cause a memory overflow.

conclusion

The proper use of ThreadLocal can store and retrieve local variables of a thread in a multi-threaded environment. However, since the key in the ThreadLocalMap field of a thread is a weakly referenced ThreadLocal object, it is prone to memory leaks during garbage collection. After each thread finishes using a ThreadLocal, it is necessary to call the ThreadLocal’s remove() method in time to release memory and prevent memory leaks from piling up and causing a memory overflow.