preface
A few days ago, I wrote an article related to AQS: I drew 35 diagrams just to let you go deep into AQS, and it got a good response. This time, I will write another article about ThreadLocal, which also goes deep into the principle and is illustrated well.
Full text of 10000+ words, 31 pictures, this article also spent a lot of time and energy to create the completion of the original is not easy, thank you.
Your first reaction to a ThreadLocal might be simple: a copy of a thread’s variables, isolated for each thread. So here are a few things you can think about:
- If the key of a ThreadLocal is a weak reference, will the key be null after GC in threadlocale.get ()?
- ThreadLocalMap data structure in ThreadLocal?
- Hash algorithm for ThreadLocalMap?
- How to resolve Hash conflicts in ThreadLocalMap?
- ThreadLocalMap Capacity expansion mechanism?
- How to clean expired keys in ThreadLocalMap? Exploratory and heuristic clean-up processes?
- ** ThreadLocalmap.set ()**
- ** ThreadLocalmap.get ()**
- How is ThreadLocal used in your project? Potholes?
- .
Do you know all of the above already? This article will take a graphical look at ThreadLocal around these issues.
The full directory
- ThreadLocal code demo
- ThreadLocal data structure
- Is the key null after GC?
- ** threadlocal.set ()**
- ThreadLocalMap Hash algorithm
- ThreadLocalMap Hash conflict
- * *, * * ThreadLocalMap. The set ()
** ThreadLocalmap.set ()** ThreadLocalmap.set ()** threadLocalmap.set () ** ThreadLocalmap.get () ¶ ThreadLocalMap.get() ¶ InheritableThreadLocal 13. InheritableThreadLocal 13.1 ThreadLocal Application Scenario 13.2 Distributed TraceId Solution
Note: This article source based on JDK 1.8
ThreadLocal code demo
Let’s take a look at an example of ThreadLocal in use:
public class ThreadLocalTest {
private List<String> messages = Lists.newArrayList();
public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);
public static void add(String message) {
holder.get().messages.add(message);
}
public static List<String> clear(a) {
List<String> messages = holder.get().messages;
holder.remove();
System.out.println("size: " + holder.get().messages.size());
return messages;
}
public static void main(String[] args) {
ThreadLocalTest.add("Is a flower romantic?"); System.out.println(holder.get().messages); ThreadLocalTest.clear(); }}Copy the code
Print result:
[A flower is considered romantic] size:0
Copy the code
ThreadLocal objects can provide thread-local variables. Each Thread has a copy of its own variable.
ThreadLocal data structure
Thread class a type for ThreadLocal. ThreadLocalMap threadLocals instance variables, that is to say, each Thread has an own ThreadLocalMap.
A ThreadLocalMap has its own separate implementation and can simply treat its key as a ThreadLocal and its value as a value put into the code (the key is not actually a ThreadLocal itself, but a weak reference to it).
When a thread puts a value into a ThreadLocal, it stores the value into its own ThreadLocalMap, and reads the value using ThreadLocal as a reference to find the corresponding key in its own map, thus achieving thread isolation.
ThreadLocalMap has a similar structure to a HashMap, except that HashMap is implemented by an array + list, whereas ThreadLocalMap does not have a list structure.
We also notice Entry, whose key is ThreadLocal
k, inheriting from WeakReference, which is also known as WeakReference type.
Is the key null after GC?
In response to the question at the top, ThreadLocal’s key is a weak reference, so is the key null after GC in threadlocale.get ()?
To understand this, we need to understand the four Java reference types:
- Strong references: We often create objects that are strong references. As long as strong references exist, the garbage collector will never reclaim the referenced object, even when memory is low
- SoftReference: an object with a SoftReference modifier is called a SoftReference. The object to which a SoftReference points is reclaimed when its memory is about to overflow
- Weak references: objects decorated with WeakReference are called weak references. Whenever garbage collection occurs, if the object is only pointed to by weak references, it will be collected
- Virtual References: Virtual references are the weakest references and are defined in Java using PhantomReference. The only purpose of a virtual reference is to queue up notifications that an object is about to die
Then look at the code, the way we use reflection to see data in a ThreadLocal after GC: (the following source code from: blog.csdn.net/thewindkee/…).
public class ThreadLocalDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(()->test("abc".false));
t.start();
t.join();
System.out.println("-- after gc -");
Thread t2 = new Thread(() -> test("def".true));
t2.start();
t2.join();
}
private static void test(String s,boolean isGC) {
try {
new ThreadLocal<>().set(s);
if (isGC) {
System.gc();
}
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true); Object threadLocalMap = field.get(t); Class<? > tlmClass = threadLocalMap.getClass(); Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if(o ! =null) { Class<? > entryClass = o.getClass(); Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("Weak reference key:%s, value :%s", referenceField.get(o), valueField.get(o))); }}}catch(Exception e) { e.printStackTrace(); }}}Copy the code
The results are as follows:
A weak reference key: Java. Lang. ThreadLocal @ 433619 b6, value: ABC weak references key: Java. Lang. ThreadLocal @ 418 a15e3, value: Java. Lang. Ref. SoftReference@bf97a12Weak reference key:nullValue: defCopy the code
Because the ThreadLocal created here does not point to any value, that is, there is no reference:
new ThreadLocal<>().set(s);
Copy the code
Referent =null; referent=null; referent=null
At first glance, if you don’t think too much about it, weak references, and garbage collection, you’ll definitely see null.
Threadlocal.get () {key = null; key = null;
If our strong reference does not exist, then the key will be recycled, which means our value will not be recycled, and the key will be recycled, causing the value to persist forever and causing a memory leak.
Threadlocal.set () ¶
A set method in a ThreadLocal is used to determine whether a ThreadLocalMap exists and then to process data.
The code is as follows:
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);
}
Copy the code
The main core logic is still in ThreadLocalMap, step by step, and dissected in more detail later.
ThreadLocalMap Hash algorithm
ThreadLocalMap implements its own hash algorithm to resolve hash array collisions.
int i = key.threadLocalHashCode & (len-1);
Copy the code
The hash algorithm in ThreadLocalMap is simple, where I is the array subscript position of the current key in the hash table.
The key here is the calculation of threadLocalHashCode, which has an attribute of HASH_INCREMENT = 0x61C88647
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode(a) {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap { 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
Each time a ThreadLocal object is created, the threadLocal. nextHashCode value increases by 0x61C88647.
This is a special value, it’s the Fibonacci number or the golden ratio number. If the hash increment is this number, the advantage is that the hash distribution is very uniform.
We can try it ourselves:
As you can see, the resulting hash codes are evenly distributed. I’m not going to go into details about The Fibonacci algorithm, but you can look it up if you’re interested.
ThreadLocalMap Hash conflict
Note: In all the following examples, the green Entry block represents normal data, and the gray Entry block represents the null key value of the Entry, which has been garbage collected. The white block indicates that Entry is NULL.
Even though ThreadLocalMap uses golden numbers as hash factors, which greatly reduces the probability of hash collisions, there are still collisions.
The way to resolve conflicts in a HashMap is to construct a list structure on an array. The conflicting data is mounted to the list and converted to a red-black tree if the list length exceeds a certain number.
However, ThreadLocalMap does not have a linked list structure, so HashMap conflict resolution is not applicable here.
As shown in the figure above, if we insert a value=27, it should hash into slot 4, which already has Entry data.
In this case, the search will be linear backward until it finds the slot with null Entry. The search will stop and the current element will be put into this slot. Of course, there are other situations in the iteration process, such as the situation where the Entry is not null and the key value is equal, and the situation where the key value in the Entry is null, and so on, they will be treated differently, which will be explained in detail later.
Here we also draw a null key in Entry (gray block data with Entry=2), which exists because the key value is a weak reference type. In the set process, if Entry data with expired keys is encountered, a round of exploratory clearing operation is actually performed, which will be described later.
ThreadLocalMap. The set (), rounding
Threadlocalmap.set (
Now that we’ve looked at the ThreadLocal hash algorithm, let’s look at how set is implemented.
There are several types of set data (adding or updating data) to a ThreadLocalMap, which are illustrated in a diagram.
In the first case, the Entry data corresponding to the slot calculated using the hash is empty:
In this case, you can directly put data into the slot.
In the second case, the slot data is not empty and the key value is the same as that obtained by the current ThreadLocal hash:
Data of the slot is directly updated here.
In the third case, the slot data is not empty. No Entry whose key has expired is encountered before the slot with null Entry is found during the subsequent traversal:
If a slot with null Entry is found, the data is put into the slot. Alternatively, if a data with the same key value is encountered during the traversal, the data can be directly updated.
In the fourth case, the slot data is not empty. During the subsequent traversal, an Entry with an expired key is encountered before finding a slot with a null Entry, as shown in the following figure. During the subsequent traversal, the key of an Entry with an index=7 is null:
If the Entry key of the hash array with subscript 7 is null, it indicates that the key value has been garbage collected. In this case, the replaceStaleEntry() method is executed, which replaces the logic of expired data and traverses from index=7 bits to perform exploratory data cleaning.
Initialize slotToExpunge = staleSlot = 7
Start with the current staleSlot and iterate forward to find other expired data, then update the expired data to start the slotToExpunge scan. The for loop iterates until it hits null Entry.
If stale data is found, the iteration continues until it hits a slot with Entry= NULL, as shown in the figure below, where slotToExpunge is updated to 0:
The current object (index=7) is iterated forward to check whether there is expired Entry data. If so, the slotToExpunge value is updated. The probe ends when null is encountered. SlotToExpunge is updated to 0.
The forward iteration above is to update the slotToExpunge starting subscript for probing and cleaning expired data, which is used to determine whether there are expired elements before the current expired slot staleSlot, as explained later.
Then start iterating backwards at staleSlot position (index=7). If an Entry with the same key value is found:
Find the Entry element with the same key value from the current node staleSlot, update the Entry value after finding it, exchange the staleSlot element location (staleSlot location is the expired element), update the Entry data, and then start to clean expired entries, as shown in the figure below:
If no Entry data with the same key value is found during the backward traversal:
The current node staleSlot looks back for entries with equal key values until the search stops when the Entry is null. The preceding figure shows that no Entry with the same key value exists in the table.
Create a new Entry and replace the table[stableSlot] position:
ExpungeStaleEntry () and cleanSomeSlots() are the two main methods used to clean up expired elements after the replacement, which will be covered in more detail later.
Threadlocalmap.set (
Set () : set(); set() : set();
java.lang.ThreadLocal.ThreadLocalMap.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(); }Copy the code
In this case, the key is used to calculate the corresponding position in the hash table. Then, the bucket corresponding to the current key is searched backwards to find the bucket that can be used.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
Copy the code
Under what circumstances is a bucket usable?
k = key
Note It is a replacement operation and can be used- When an expired bucket is encountered, the replacement logic is performed to occupy the expired bucket
- In the process of searching, I hit the bucket
Entry=null
In the case of direct use
NextIndex (), prevIndex(); nextIndex(); prevIndex();
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1> =0)? i -1 : len - 1);
}
Copy the code
Now look at the logic in the rest of the for loop:
- Traverse the current
key
Value in the bucketEntry
The data is empty, which means there’s no data collision in the hash array. Pop outfor
Loop, directset
Data is stored in the corresponding bucket - if
key
Value in the bucketEntry
Data is not null
2.1 If k = key, the current set operation is a replacement operation and the replacement logic is directly returned. 2.2 If key = NULL, the Entry of the current bucket location is expired. Execute replaceStaleEntry() (core method). 3. The for loop completes and continues. The entry is null during the backward iteration 3.1 Creating a new entry object in a bucket with a null entry 3.2 Performing the ++ SIZE operation 4. Call cleanSomeSlots() to do a heuristic cleanup of data whose Entry keys have expired in the hash array 4.1 If no data has been cleaned up after the cleanup and the size exceeds the threshold (2/3 of the array length), In 4.2 rehash(), a round of exploratory cleanup will be performed to clean expired keys. After the cleanup, if size >= threshold-threshold / 4, the actual expansion logic will be performed (see the expansion logic later).
The replaceStaleEntry() method provides the function to replace expired data. We can review the schematic of the fourth case above. The code is as follows:
java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry()
:
private void replaceStaleEntry(ThreadLocal<? > key, Object value,int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (inti = prevIndex(staleSlot, len); (e = tab[i]) ! =null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (inti = 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); }Copy the code
SlotToExpunge indicates the start index of probing to clear expired data. By default, the index starts from the current staleSlot. Start with the current staleSlot and iterate forward to find data that has not expired. The for loop does not end until Entry is null. If the expired data is found ahead, the update probe clears the expired data with the starting subscript I, that is, slotToExpunge= I
for (inti = prevIndex(staleSlot, len); (e = tab[i]) ! =null;
i = prevIndex(i, len)){
if (e.get() == null){ slotToExpunge = i; }}Copy the code
Then start looking backwards from staleSlot and end up hitting a bucket with null Entry. If k == key is encountered during iteration, this indicates that the replacement logic is in place, replacing the new data and swapping the current staleSlot position. If slotToExpunge == staleSlot, this means that replaceStaleEntry() did not find expired Entry data when it first looked forward and then did not find expired Entry data when it looked back. Change the index of the current cycle to slotToExpunge = I. Finally call cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); Perform heuristic expiration data cleansing.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
Copy the code
The cleanSomeSlots() and expungeStaleEntry() methods, which are detailed later, are related to cleanup. The first is heuristic cleanup of expired key related entries (heurscan), The other is exploratory cleaning of expired key-related entries.
If k! K == null indicates that the current Entry traversed is an expired data. SlotToExpunge == staleSlot indicates that no expired Entry was found in the initial forward search. If the condition is correct, slotToExpunge is updated to the current position. The prerequisite is that no expired data is found during the scan of the precursor node.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
Copy the code
In the next iteration, if no data with k == key is found and data with Entry NULL is encountered, the current iteration operation will be terminated. Add new data to the slot corresponding to table[staleSlot].
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
Copy the code
In addition to staleSlot, other expired slot data is found, and the logic of clearing data should be enabled:
if(slotToExpunge ! = staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);Copy the code
Exploratory clean-up process for ThreadLocalMap expired keys
There are two types of stale key data cleanup mentioned in ThreadLocalMap: exploratory cleanup and heuristic cleanup.
The expungeStaleEntry method traverses the hash array, probes and cleans expired data backwards from the starting position, sets the Entry of expired data to NULL, and rehash unexpired data to locate it in the table array. If the location already has data, the unexpired data will be added to the bucket with Entry= NULL closest to the location, making the rehash Entry data closer to the correct location of the bucket. The operation logic is as follows:
As shown in the figure above, after hash calculation, set(27) should fall into bucket index=4. Since bucket index=4 already has data, the final data in subsequent iterations will be put into bucket index=7. After a period of time, Entry data key in bucket index=5 will become null
If additional data sets are added to the map, a probe cleanup operation is triggered.
As shown in the figure above, after performing exploratory cleaning, the index=5 is cleared. Continue iterating until the element with index=7 is rehashed and the correct index=4 is found. Find the node with the nearest Entry= NULL to index=4 (the data just cleared by probe: index=5) and move the data with index= 7 to index=5. At this time, the bucket position is closer to the correct position index=4.
After a round of exploratory cleaning, the data with expired keys will be cleaned up. The bucket position of the unexpired data after rehash is theoretically closer to the position of I = key.hashcode & (tab.len-1). This optimization improves overall hash query performance.
Then look at the specific process of expungeStaleEntry(), we will sort out step by step in the way of schematic diagram and source code explanation:
We call this method with expungeStaleEntry(3), as shown in the figure above. We can see the table data in ThreadLocalMap, and then clean up:
The first step is to clear the data for the current staleSlot position, and the Entry at index=3 becomes NULL. And then further probe:
After the second step, the element with index=4 is moved to the slot with index=3.
If normal data is encountered, the position of the data is calculated. If it is offset, the slot position is recalculated. The purpose is to store normal data in the correct location or closer to the correct location as possible
A round of exploratory cleaning is complete when an empty slot is encountered during the next iteration, and we continue to look at the implementation source code:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
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;
}
Copy the code
Empty the TAB [staleSlot] slot, then set size– then iterate through the staleSlot position, and empty the slot if k==null expires, then size–
ThreadLocal<? > k = e.get();if (k == null) {
e.value = null;
tab[i] = null;
size--;
}
Copy the code
If the key does not expire, the system recalculates whether the subscript position of the current key is the subscript position of the current slot. If it is not, a hash conflict occurs. In this case, the system iterates through the correct slot position to find the latest position that can store the entry.
int h = k.threadLocalHashCode & (len - 1);
if(h ! = i) { tab[i] =null;
while(tab[h] ! =null)
h = nextIndex(h, len);
tab[h] = e;
}
Copy the code
Normal Hash conflicted data is processed here. After iteration, the Entry position of Hash conflicted data will be closer to the correct position. In this way, the query efficiency will be higher.
ThreadLocalMap Capacity expansion mechanism
At the end of the threadLocalmap.set () method, if no data has been cleaned up after the heuristic and the number of entries in the current hash array has reached the list expansion threshold (len*2/3), the rehash() logic is executed:
if(! cleanSomeSlots(i, sz) && sz >= threshold) rehash();Copy the code
Let’s look at the rehash() implementation:
private void rehash(a) {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
private void expungeStaleEntries(a) {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if(e ! =null && e.get() == null) expungeStaleEntry(j); }}Copy the code
Here, exploratory cleaning will be carried out first, cleaning from the start position of the table back, there is a detailed analysis of the cleaning process above. After the table is cleared, some entries with null keys may be cleared. In this case, determine whether to expand the table by size >= threshold-threshold /4, that is, size >= threshold* 3/4.
Rehash () : size >= threshold
Let’s look at the resize() method in detail. To illustrate, we’ll use oldTab.len=8:
The size of the expanded TAB is oldLen * 2, and then the old hash table is iterated, the hash position is recalculated, and the new TAB array is placed. If a hash conflict occurs, the slot with the most recent entry is null is searched later. All entry data from the oldTab has been put into the new TAB. Re-calculate the threshold for the next TAB expansion. The code is as follows:
private void resize(a) {
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;
} 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
ThreadLocalMap. The get (), rounding
Set () = set(); set() = set();
Diagram of ThreadLocalMap. The get ()
If the Entry. Key in the slot is the same as the sought key, the following information is returned:
The Entry key in the slot position is inconsistent with the desired key:
Take get(ThreadLocal1) as an example. After hash calculation, the correct slot position is 4. The index=4 slot already contains data, and the key value is different from ThreadLocal1.
ExpungeStaleEntry () is executed. After executing the expungeStaleEntry() method, the data in index 5 and 8 will be reclaimed, while the data in index 6 and 7 will be moved forward. At this point, the next iteration will continue. When index = 6, the Entry data with the same key value is found, as shown in the figure below:
Threadlocalmap.get (
java.lang.ThreadLocal.ThreadLocalMap.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);
}
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
Heuristic cleanup process for ThreadLocalMap expired keys
As mentioned several times above, ThreadLocalMap expiration can be cleaned up in two ways: exploratory (expungeStaleEntry()) and heuristic (cleanSomeSlots()).
Probe clearing is linear probe clearing, which is performed after the current Entry and ends when the value is null.
Heuristic cleanup is defined by the author as “Heuristically scan some cells looking for stale entries.”
The specific code is as follows:
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 && e.get() == null) {
n = len;
removed = true; i = expungeStaleEntry(i); }}while ( (n >>>= 1) != 0);
return removed;
}
Copy the code
InheritableThreadLocal
When we use ThreadLocal, there is no way to share a thread copy created by the parent thread with the child thread in an asynchronous scenario.
To solve this problem, there is also an InheritableThreadLocal class in the JDK. Let’s look at an example:
public class InheritableThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
threadLocal.set("Parent data :threadLocal");
inheritableThreadLocal.set("Parent class data :inheritableThreadLocal");
new Thread(new Runnable() {
@Override
public void run(a) {
System.out.println("Child thread gets parent threadLocal data:" + threadLocal.get());
System.out.println(The child thread gets the inheritableThreadLocal data from the parent class:+ inheritableThreadLocal.get()); } }).start(); }}Copy the code
Print result:
The child thread gets the parent threadLocal data:nullThe child thread gets the inheritableThreadLocal data from the parent classCopy the code
The child Thread is created by calling the new Thread() method in the parent Thread. The Thread#init method is called in the Thread constructor. Copy parent thread data to child thread in init method:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
if(inheritThreadLocals && parent.inheritableThreadLocals ! =null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID();
}
Copy the code
[InheritableThreadLocal] [init()] [Thread pool] [InheritableThreadLocal] [init()] [Thread pool]] [Thread pool] [init()]] [Thread pool] So there’s a problem here.
Of course, if there is any problem, there will be a solution. If there is any TransmittableThreadLocal, alibaba has opened an open source module to solve this problem.
Use actual combat in ThreadLocal projects
Usage scenarios of ThreadLocal
At present, we use ELK+Logstash to record logs in the project. Finally, we display and retrieve logs in Kibana.
Nowadays, distributed systems provide services uniformly. The invocation relationship between projects can be associated with traceId. However, how can traceId be transmitted between different projects?
We use org.slf4j.mdc to do this, internally via ThreadLocal, as follows:
When the current end sends A request to service A, service A generates A traceId string similar to A UUID and puts the string into the ThreadLocal of the current thread. When service B calls, service A writes traceId into the Header of the request. When receiving a request, service B checks whether the Header of the request contains a traceId and writes the traceId to the ThreadLocal of its own thread.
In the figure, requestId is the traceId associated with each system link. Systems call each other, and corresponding links can be found through this requestId. Here are some other scenarios:
For each of these scenarios, we can have corresponding solutions, as shown below
Feign remote call solution
The service sends a request:
@Component
@Slf4j
public class FeignInvokeInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String requestId = MDC.get("requestId");
if (StringUtils.isNotBlank(requestId)) {
template.header("requestId", requestId); }}}Copy the code
The service receives requests:
@Slf4j
@Component
public class LogInterceptor extends HandlerInterceptorAdapter {
@Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) {
MDC.remove("requestId");
}
@Override
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) {}@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestId = request.getHeader(BaseConstant.REQUEST_ID_KEY);
if (StringUtils.isBlank(requestId)) {
requestId = UUID.randomUUID().toString().replace("-"."");
}
MDC.put("requestId", requestId);
return true; }}Copy the code
Thread pool asynchronous call, requestId pass
Since MDC is implemented based on ThreadLocal, child threads do not have access to data stored by parent ThreadLocal threads during asynchronous processing, so we can customize the thread pool executor by modifying the run() method:
public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public void execute(Runnable runnable) {
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(() -> run(runnable, context));
}
@Override
private void run(Runnable runnable, Map<String, String> context) {
if(context ! =null) {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally{ MDC.remove(); }}}Copy the code
Use MQ to send messages to third-party systems
Customize the property requestId in the message body sent by MQ. After the message is consumed by the recipient, the requestId can be used by the recipient.