One, foreword
ThreadLocal allows each thread to access its own local variable to avoid concurrency problems.
ThreadLocal is used a lot in everyday work, such as database connections, sessions, cookies, and other thread-level caching. It’s also a common question in interviews, how does ThreadLocal work? Why does a memory leak occur? How to solve it?
2. The basic structure of ThreadLocal
Code examples:
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
try {
threadLocal.set("xxx");
threadLocal.get();
} finally {
threadLocal.remove();
}
Copy the code
It’s really simple. There are only 3 ways. Go deep into the source, found that each Thread to maintain a ThreadLocal. ThreadLocalMap threadLocals types of variables, which is to store the key – the value of the container.
public class Thread implements Runnable {... . ThreadLocal.ThreadLocalMap threadLocals =null; . . }Copy the code
ThreadLocalMap is an internal class of ThreadLocal, and there is an internal class Entry of ThreadLocalMap that inherits WeakReference and stores key-Valued. From the inheritance relationship, key is an object reference of ThreadLocal type. And is a weak reference (it is important to remember that key is a weak reference).
ThreadLocalMap (ThreadLocalMap) is a simple version of a HashMap. The basic ideas of a HashMap are as follows: the capacity of a HashMap must be raised to the power of 2, expansion, hash mapping, and open addressing for resolving hash conflicts.
public class ThreadLocal<T> {... .static class ThreadLocalMap {
static class Entry extends WeakReference<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 entries in 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) {
// Expand the factor by 2/3
threshold = len * 2 / 3; }... . }... . }Copy the code
Third, the set
Gets the ThreadLocalMap of the current thread, if not null, otherwise initializes ThreadLocalMap and sets the value.
// java.lang.ThreadLocal#set
public void set(T value) {
Thread t = Thread.currentThread();
// Get the ThreadLocalMap of the current thread
// ThreadLocalMap is an internal class of ThreadLocal, as a variable per thread
ThreadLocalMap map = getMap(t);
if(map ! =null)
// The current instance object of ThreadLocal is referenced as the key of ThreadLocalMap (weak reference)
map.set(this, value);
else
// map=null, the first time set, initialization
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
Copy the code
1. Initialize ThreadLocalMap
If map is null, the initial capacity is 16 and cannot be changed externally. The expansion factor is 2/3.
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Copy the code
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);
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
Copy the code
2. Set the value
Map #set(ThreadLocal
key, Object value), ThreadLocal instance Object reference as key:
private void set(ThreadLocal
key, Object value) {
Entry[] tab = table;
int len = tab.length;
// hash mapping
int i = key.threadLocalHashCode & (len-1);
for(Entry e = tab[i]; e ! =null;
// Resolve hash conflicts: open addressing
e = tab[i = nextIndex(i, len)]) {
// I position e! =null hash conflictThreadLocal<? > k = e.get();if (k == key) {
// the key is equal to the key
e.value = value;
return;
}
/ / k = null,
if (k == null) {
// Replace the old entry to prevent memory leaks
// The process of cleaning up obsolete data is optimistic, but every effort is made to clean up obsolete data
// If the set is scanned in a proportional manner, the set can be scanned in a proportional manner
replaceStaleEntry(key, value, i);
return; }}// Create a new entry at position I without hash conflict
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots scales and returns Boolean, true for stale data, false for no stale data
// If old data is not cleared! False If the number reaches the capacity expansion threshold, expand the capacity
// Clean up old data! True (also to avoid unnecessary expansion)
if(! cleanSomeSlots(i, sz) && sz >= threshold)/ / capacity
rehash();
}
Copy the code
(1) First obtain the hash value of the key and map it to Entry[] to find the position I to be set. Calculating the hash value of the key is also very simple. Maintain an AtomicInteger variable and add HASH_INCREMENT to each hash. Because the array size is an integer of 2, efficient & operations can be used instead of modular operations.
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode(a) {
// AtomicInteger adds HASH_INCREMENT every time
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
Copy the code
(2) If I is found, we will see if I is empty. If I is not empty, we will see if there is a hash conflict.
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
Copy the code
If k=null, weak reference k is collected by gc and value is strongly referenced, which may cause a memory leak. If k=null, weak reference K is collected by GC and value is strongly referenced. So replace the stale entry (k=null, entry! =null indicates an outdated entry.
ReplaceStaleEntry:
This replacement is not a simple replacement. It will also scan whether there are any positions (slotToExpunge) in front of the replacement position staleSlot, which is an outdated entry that needs to be cleaned up. It will also scan the position (I) after staleSlot to see if there are any positions where the old K and the new key are equal. Check whether slotToExpunge needs to be updated.
private void replaceStaleEntry(ThreadLocal<? > key, Object value,int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// staleSlot position, key=null, is an old value that should be replaced and deleted
// Scan staleSlot for slotToExpunge to be deleted
// Stop scanning until an empty position is encountered. This is also an optimistic scan, believing that there is no entry that can be cleared
int slotToExpunge = staleSlot;
for (inti = prevIndex(staleSlot, len); (e = tab[i]) ! =null;
i = prevIndex(i, len))
// The key of e is null.
if (e.get() == null)
slotToExpunge = i;
// The purpose of this loop is to scan staleSlot to see if there is a slot behind staleSlot with the same key, if there is a staleSlot and I swap
for (inti = nextIndex(staleSlot, len); (e = tab[i]) ! =null; i = nextIndex(i, len)) { ThreadLocal<? > k = e.get();if (k == key) {
// Find key equal, old and new swap
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Delete the value of slotToExpunge
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// Delete the value of slotToExpunge and scan for other outdated entries
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// slotToExpunge is used to find out if there are other locations that need to be cleaned besides staleSlot
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// If no equal key is found, staleSlot position put new entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
// If there are other entries that need to be cleared, the entries are cleared
if(slotToExpunge ! = staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }Copy the code
Finally slotToExpunge! = staleSlot, which indicates that there are other locations that are outdated and need to be cleaned up, and cleanSomeSlots is called.
CleanSomeSlots calls expungeStaleEntry first. The relationship between expungeStaleEntry and cleanSomeSlots is that expungeStaleEntry deletes the entry to the specified location, while cleanSomeSlots scans other locations to see if there are any outdated entries that need to be cleaned.
ExpungeStaleEntry:
The system deletes the entry at the specified position and searches backwards to see if there are any other outdated entries. In addition, the system hashes the entries that are not outdated to make the positions in the array coherent and tidy. The return value is the index of the empty position next to the deleted position.
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;
// Start from staleSlot and traverse backwards to check if k==null for non-empty positions
for(i = nextIndex(staleSlot, len); (e = tab[i]) ! =null;
i = nextIndex(i, len)) {
// Set key to null to prevent memory leakageThreadLocal<? > k = e.get();if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// The key is not null. Rehash the remaining elements of the array
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; }}}// the index of the next null slot after staleSlot
// all between staleSlot and this slot will have been checked
// for expunging
return i;
}
Copy the code
CleanSomeSlots:
The purpose of cleanSomeSlots is to clean up as many outdated entries as possible. The first parameter I must not be the location that needs to be cleaned up. Is there any other location after the open address I that needs to be cleaned up? However, this process may affect the performance of the set, and the time complexity is O(n), so it is unlikely to scan the full table, but to scan the scale.
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
// cleanSomeSlots is designed to remove all outdated entries,
// However, this process may cause the set to be time-consuming, O(n) time complexity
// So scan the scale to remove outdated entries,
// Len =16,16/2=8,8/2=4,4/2=2,2/2=1,1/2=0
// The ratio is not even
do {
i = nextIndex(i, len);
Entry e = tab[i];
if(e ! =null && e.get() == null) {
// Reset n to len
n = len;
removed = true; i = expungeStaleEntry(i); }}while ( (n >>>= 1) != 0);
return removed;
}
Copy the code
(3) It is found that the position of I is empty, indicating that no hash conflict has occurred. Create an Entry at position I and add size+1.
tab[i] = new Entry(key, value);
int sz = ++size;
Copy the code
(4) Determine whether capacity expansion is required
Two conditions need to be met for capacity expansion. The size of elements reaches the capacity expansion threshold. Data is not cleaned in the process of cleanSomeSlots, which is also to avoid unnecessary capacity expansion.
Only one element can be added at a time. After the preceding ++size, it just reaches the threshold, that is, size+1 >=threshold meets the requirement. If cleanSomeSlots cleans up outdated data, size+1-x must be less than threshold, so there is no need to expand.
if(! cleanSomeSlots(i, sz) && sz >= threshold)/ / capacity
rehash();
Copy the code
If capacity expansion is required, call rehash(). Before capacity expansion, scan the entire table to clear outdated data. If this fails to increase the free space, expand resize.
private void rehash(a) {
// Clear outdated data before capacity expansion
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// 2/3-2/3*1/4= 1/2 By the end of the expansion threshold becomes 1/2, for encoding lag? Don't get it...
if (size >= threshold - threshold / 4)
resize();
}
Copy the code
Resize: Create an array twice as long as the old array, iterate over the old array and hash over to the new array.
/** * Double the capacity of the table. * copy on write, only ensure final consistency */
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; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while(newTab[h] ! =null) h = nextIndex(h, newLen); newTab[h] = e; count++; }}}// Set a new threshold
setThreshold(newLen);
size = count;
table = newTab;
}
Copy the code
Fourth, the get
After analyzing set, get is much simpler. Since the open addressing is used to resolve hash conflicts, if the hash map finds a position of I that is not the value to be found, it needs to address backward. If outdated data is encountered during the traversal process, expungeStaleEntry is called to clear it, which can avoid memory leakage to a certain extent.
public T get(a) {
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; }}// Not found, do initialization
return setInitialValue();
}
Copy the code
private Entry getEntry(ThreadLocal
key) {
// Hash map to find I, open addressing if I is not the element to be found
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if(e ! =null && e.get() == key)
return e;
else
// Open addressing
return getEntryAfterMiss(key, i, e);
}
Copy the code
private Entry getEntryAfterMiss(ThreadLocal<? > key,int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// open the address to find a value with the same key
while(e ! =null) { ThreadLocal<? > k = e.get();if (k == key)
return e;
// If k is null, delete it to prevent memory leakage
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
Copy the code
Fifth, remove
Remove the ThreadLocal instance reference as key, set key to NULL, and value to null. This disconnects the strong reference to value and allows gc to reclaim it.
public void remove(a) {
ThreadLocalMap m = getMap(Thread.currentThread());
if(m ! =null)
m.remove(this);
}
Copy the code
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) {
// Set key to null
e.clear();
// Delete the old entry,value=null
expungeStaleEntry(i);
return; }}}//java.lang.ref.Reference#clear
public void clear(a) {
this.referent = null;
}
Copy the code
6. Memory leakage
When using ThreadLocal, you must always call remove to avoid memory leaks.
1. Memory leak demo
We can simulate heap memory usage without and with remove.
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
ThreadLocal<User> threadLocal = new ThreadLocal<>();
try {
threadLocal.set(new User());
threadLocal.get();
} finally {
// threadLocal.remove();
}
TimeUnit.MINUTES.sleep(5);
}
static class User {
private byte[] datas = new byte[1024*1024*100];
}
private static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}
Copy the code
Run the above code, open the JDK Java/jdk1.8.0_241/ binjVisualvm. exe to check the heap memory usage:
(1) The following figure is not calledremove
After clicking on the heap to perform garbage collection, 100MB of garbage will still not be collected. This is a memory leak.(2) The following figure is calledremove
After the garbage collection is executed, the heap memory is almost 0, indicating that the garbage collection is basically completed.
2. Causes of memory leaks
Don’t callremove
Why is there a memory leak? If you read the source code carefully,ThreadLocal
Is referenced as an inner classThreadLocalMap
Key, and this key is a weak reference (weak references will be collected in the next garbage collection),
sokey
It will be recycled in the next garbage collection, andvalue
Strong references are still there,As long as the current thread is not destroyed.value
It will never be garbage collected, so it will leak memory.
How to avoid memory leaks, of course, is to write code, remember remove. The correct way to call remove:
3, set and GET official avoid memory leaks
ThreadLocal’s set and GET operations clean up stale data to a certain extent (key= NULL) to prevent memory leaks, but there is no guarantee that all stale data will be cleaned up, so don’t count on set and GET.
Seven,
ThreadLocal
Give each thread access to its own local variables to ensure thread-safety.- Each thread maintains one
ThreadLocalMap
The easy versionHashMap
.key
forThradLocal
For example, the way to resolve hash conflicts is open addressing, so it is not suitable for storing large amounts of data. - The get and SET processes clear outdated data. However, not all outdated data may be cleared. Otherwise, normal operation performance may be affected.
- Using the
ThreadLocal
Be sure to callremove
Otherwise, there will be a memory leak, because key is a weak reference and will be gc next time, while value will always have a strong reference. If the thread is not destroyed, value will never GC out, resulting in a memory leak.
PS: If there is any misunderstanding in this article, we welcome your criticism and correction, and look forward to your comments, likes and favorites. I am Xu, willing to make progress with you!
Sky blue and so on misty rain, and I am waiting for you, wechat public number search: Xu students ah, continue to update liver goods, come to pay attention to me, and I study together ~