The role of ThreadLocal
Instead of using various locking mechanisms to access variables, the idea of a ThreadLocal is to exchange space for time so that each thread can access its own copy of variables without interfering with each other. Reduce the complexity of passing some common variables between functions or components in the same thread.
The use of ThreadLocal
ThreadLocal contains the following methods:
The get function is used to get the value of the ThreadLocal associated with the current thread. If the current thread does not have a value of this ThreadLocal, the initialValue function is called to get the initialValue and return it. InitialValue is protected. So generally when we use it, we need to inherit the function and give the initial value. The set function is used to set the value of this ThreadLocal for the current thread, and the remove function is used to remove the value of the ThreadLocal binding. In some cases, this function needs to be called manually to prevent memory leaks.
Code sample
public class Main {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
returnInteger.valueOf(0); }}; public static void main(String[] args) { Thread[] threads = new Thread[5];for(int i=0; i<5; i++) { threads[i] = new Thread(newRunnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() +"'s initial value: " + threadLocal.get());
for(int j=0; j<10; j++) { threadLocal.set(threadLocal.get() + j); } System.out.println(Thread.currentThread() +"'s last value: "+ threadLocal.get()); }}); }for(Thread t: threads) t.start(); }}Copy the code
In the above example, threadLocal is unique to each thread, so the values of each threadLocal do not affect each other even after they are added up. The output is as follows:
ThreadLocal source code analysis
Having seen the basics of ThreadLocal usage, the reader must be wondering how ThreadLocal is implemented internally. The main content of this article is to walk you through the internal implementation of ThreadLocal using Java 1.8-based code. Take a look at the picture below:
ThreadLocalMap
static class Entry extends WeakReference<ThreadLocal<? >> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<? > k, Object v) { super(k); value = v; }}Copy the code
Here, the line between ThreadLocal and key is dashed, because Entry inherits WeakReference implementation. When ThreadLocal Ref is destroyed, the only strong reference to the ThreadLocal instance in the heap disappears. Only Entry has a weak reference to a ThreadLocal instance, which can be removed by GC, assuming you know the nature of weak references. In this case, the key in the Entry is null, and the value in the Entry cannot be reclaimed until the thread ends. In this case, memory leaks may occur.
With the general data structure in mind, let’s explore the implementation of ThreadLocal in a few specific ways:
The 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;
returnresult; }}return setInitialValue();
}
Copy the code
Directly look at the code, you can analyze the following steps:
- Get the current Thread object, using getMap to get ThreadLocalMap within the Thread
- If the map already exists, get the Entry object with the current ThreadLocal as the key and extract the value from the Entry
- Otherwise, call setInitialValue for initialization.
Let’s look at some of the functions specifically mentioned above:
getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
Copy the code
ThreadLocalMap = ThreadLocalMap = ThreadLocalMap = ThreadLocalMap;
ThreadLocal.ThreadLocalMap threadLocals = null;
Copy the code
So a ThreadLocalMap is defined in a ThreadLocal. We’ve already talked about the Entry definition of a ThreadLocalMap. We’ll put setInitialValue first.
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;
}
Copy the code
SetInititialValue is called when the Map does not exist
- InitialValue is called to generate an initialValue. Further into the initialValue function, we know that it returns null.
- If the Map exists, map.set is used.
- If they don’t exist, createMap is called to create a ThreadLocalMap.
ThreadLocalMap
The definition of the createMap method is simple:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Copy the code
A ThreadLocalMap constructor is called to generate a map.
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<? >> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<? > k, Object v) { super(k); value = v; } } /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; /** * The number of entriesin the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
Copy the code
ThreadLocalMap is defined as a static class containing the main members:
- The first is the definition of Entry, which has been mentioned above.
- The initial capacity is zero
INITIAL_CAPACITY = 16
; - The main data structure is an array table of entries;
- Size records the number of entries in the Map.
- Threshold is the upper limit of capacity expansion. When size reaches threashold, the entire Map needs to be resized. The initial value of threshold is
len * 2 / 3
; - NextIndex and prevIndex are used to move indexes safely, and are often used in later functions.
The ThreadLocalMap constructor looks like this:
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);
}
Copy the code
It uses firstKey and firstValue to create an Entry, calculates index I, inserts the Entry into position I in the table, and sets size and threshold.
GetEntry: getEntry: getEntry: getEntry: getEntry: getEntry: getEntry
map.getEntry
private Entry getEntry(ThreadLocal<? > key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i];if(e ! = null && e.get() == key)return e;
else
return getEntryAfterMiss(key, i, e);
}
Copy the code
- Hash %(table. Length-1); hash%(table.
- If an Entry exists and the key of the Entry happens to be ThreadLocal, the Entry object is returned directly.
- Otherwise, if no Entry is found at this location, getEntryAfterMiss is called.
This leads to the getEntryAfterMiss method:
getEntryAfterMiss
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;
}
Copy the code
And we have to combine this with the previous step, which is because we don’t satisfy e! = null && LLDB () == key, getEntryAfterMiss (LLDB () == key)) So we go into the while loop, which is a loop over and over, and if e is never empty, we call nextIndex, incrementing I, and we make two judgments all the time:
- if
k==key
, it means that the required Entry is found and returned directly; - if
k==null
, then prove that the key in this Entry is null, then this Entry is an expired object, called hereexpungeStaleEntry
Clear the Entry. This is the answer to a hole left in front,When a ThreadLocal Ref is destroyed, the ThreadLocal instance will be removed by GC because only one weak reference in the Entry is pointed to. The Entry key will be lost, and the value may leakIn fact, in every GET and set operation, such entries with a null key are constantly cleared.
Why loop lookup?
Here you can skip to the set method, mainly because of the way it handles hash collisions. As we all know, HashMap uses the zipper method to handle hash collisions, that is, it uses the linked list to link the conflicting elements after the elements already in place, whereas ThreadLocal uses the open address method. Place the element to be inserted after null. For details on the difference between the two methods, see the main method for resolving HASH conflicts. Therefore, the above loop is because there may not be any Entry whose key is exactly the same as the key we want to find at the position I calculated first, so we have to keep repeating the loop to check whether it is inserted behind until we find the element that is null, because if we insert it, it will end at NULL.
After analyzing the cause of the loop, you can also dig into expungeStaleEntry to see how it is cleaned.
expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
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; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale.while(tab[h] ! = null) h = nextIndex(h, len); tab[h] = e; }}}return i;
}
Copy the code
The code above has two main parts:
- Expunge Entry at staleSlot: The value of the entry at position I is set to NULL, and the reference of the entry is also set to NULL. Then the system will naturally clear the memory during GC.
- Rehash until We Encounter NULL: This is the array of entries after staleSlot and before null, and clears every Entry with a null key. If the key is not empty, Rehash the position.
Why rehash?
Because we set a value to NULL during cleanup, the area after that value that was previously attached to the previous value will only be looked up to NULL during the next iteration.
For example… ,<key1(hash1), value1>, <key2(hash1), value2>,… If <key3(hash2), value3> is inserted, the target hash position of the hash table will be occupied by <key2(hash1), value2>. , <key1(hash1), value1>, <key2(hash1), value2>, <key3(hash2), value3>, … At this point, if <key2(hash1), value2> is cleaned, obviously <key3(hash2), value3> should be moved forward (rehash), otherwise key3 will not be found if you look up the hash table with key3
Set method
The set method is also described in the loop lookup of the get method, which is called the open address 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);
}
Copy the code
Set (ThreadLocal<? > key, Object value), if not, call createMap to create.
map.set
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(a); }Copy the code
Take a look at the code above:
- First of all, I is calculated according to the key, and then the Entry of I is searched.
- If an Entry already exists and the key is equal to the key passed in, the Entry is assigned a new value.
- If an Entry exists but the key is null, replaceStaleEntry is called to replace the Entry with an empty key
- If no return is returned during the loop, create an Entry at the null location and insert it, increasing the size by 1.
- The final call to cleanSomeSlots won’t go into detail, but you need to know that the expungeStaleEntry function mentioned above is still called internally to clean up the Entry with the null key
sz>thresgold
This is the condition to determine whether the rehash function has been met. If so, the rehash function will be called.
There are two functions in this code that need to be analyzed, first of all
replaceStaleEntry
private void replaceStaleEntry(ThreadLocal<? > key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e;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;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if(slotToExpunge ! = staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }Copy the code
First, recall from the previous step that replaceStaleEntry is called because the key of the Entry at that location is null.
- In the first for loop, we find slotToExpunge where the key is null.
- The second for loop: If we find an Entry whose key is equal to the incoming key, we assign a new value to the Entry and swap it with the Entry at the staleSlot location. Then call CleanSomeSlots to clean up the Entry with a null key.
- If there is no Entry equal to the incoming key, create a new Entry at staleSlot. The function finally clears the Entry for the empty key.
After replaceStaleEntry, another important function is rehash and conditions for rehash:
Sz > threshold;
rehash
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
Copy the code
After the Entry of the empty key is cleared, if the size is greater than 3/4 threshold, the resize function is called:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if(e ! = null) { ThreadLocal<? > k = e.get();if (k == null) {
e.value = null; // Help the GC
} 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;
}
Copy the code
Then a for loop clears entries with empty keys, recalculates the hash values of entries with non-empty keys, and updates all attributes of ThreadLocalMap.
remove
The last thing to explore is the remove function, which is used to remove an unused Entry from a map. The hash value is computed first, and if it is not hit the first time, the loop is looped until null, during which expungeStaleEntry is called to remove the empty key. The code is as follows:
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; }}}Copy the code
Best practices for using ThreadLocal
We found that no matter the set,get or remove methods, the Entry with a null key will be erased, so the value in the Entry will not have a strong reference chain, and will be reclaimed during GC. So how can there be a memory leak? But the idea is to assume that you call get or set, which we don’t, so the best practice is *
1. Users need to manually call the remove function to remove ThreadLocal that is no longer needed.
2. Also try to set ThreadLocal toprivate static
ThreadLocal will try to die with the thread itself.