What is a ThreadLocal

ThreadLocal is a ThreadLocal variable that holds private data for each thread. Whenever you create a ThreadLocal variable, each thread accessing the variable stores a local copy of the variable in the current thread, which is accessible only to its own thread and not shared with other threads. This avoids the problem of thread resource sharing variable conflicts

Basic usage of ThreadLocal

public class ThreadLocalDemoOne {

    /** * create a ThreadLocal variable */
    private static ThreadLocal<Integer> intLocal = new ThreadLocal<>();

    /** * Create ThreadLocal and initialize assignment */
    private static ThreadLocal<Integer> intLocal2 = ThreadLocal.withInitial(() -> 6);

    public static void main(String[] args) {
        // Set the variable value
        intLocal.set(8);
        // Read the variable value
        System.out.println("intLocal data: " + intLocal.get());
        // Clear the variable values
        intLocal.remove();
        System.out.println("intLocal data: " + intLocal.get());
        System.out.println("intLocal data: "+ intLocal2.get()); }}Copy the code

ThreadLocal basic data structure

From the Thread class source code, can see the Thread of two ThreadLocal. ThreadLocalMap object

public class Thread implements Runnable {
    / /...
    // The ThreadLocal value associated with this thread. Maintained by the ThreadLocal class
    ThreadLocal.ThreadLocalMap threadLocals = null;

    // The InheritableThreadLocal value associated with this thread. Maintained by the InheritableThreadLocal class
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    / /...
}
Copy the code

Variables in the Thread class store variables’ private ThreadLocal values

  • ThreadLocals: value of the thread’s private ThreadLocal
  • InheritableThreadLocals: thread-private values that can be inherited by threads

ThreadLocalMap

ThreadLocalMap is a custom HashMap implemented by the ThreadLocal class

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; }}/** * Initialize space size */
    private static final int INITIAL_CAPACITY = 16;

    /** * The table, resized as necessary. * table.length MUST always be a power of two. */
    private Entry[] table;
Copy the code
  • Key: the current thread is the ThreadLocal object
  • Value: Indicates the value set by set

Note: Entry extends WeakReference > The key in the table is a weak reference. This is a point worth exploring. Why Java designs the key as a weak reference? >

Conclusion: The final variable is placed in the current thread’s ThreadLocalMap, not in ThreadLocal. ThreadLocal can be understood as a wrapper of ThreadLocalMap, passing the variable value.

InheritableThreadLocal

InheritableThreadLocal is primarily used to pass ThreadLocal objects from the main thread to child threads

public class InheritableThreadLocalDemo {

    private static ThreadLocal<Integer> intLocal= new ThreadLocal<>();
    private static InheritableThreadLocal<Integer> intInheritableLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {

        intLocal.set(1);
        intInheritableLocal.set(2);

        Thread thread = newThread.out.println (thread.currentThread ().getName() + ":" + intLocal.get()); System.out.println(thread.currentThread ().getName() + ":" + intinHeritabLelocal.get ()); }); thread.start(); }}Copy the code

The execution result

Thread-0: indicates null thread-0:2Copy the code

You can see that an InheritableThreadLocal object is declared that can be inherited by the quilt thread.

The ThreadLocal memory leaks

Memory leaks

After applying for memory, the program cannot release the memory space that has been applied for. The damage caused by a memory leak can be ignored. However, the accumulation of memory leaks is very serious

Many articles I have read say that memory leaks are caused by weak references to the key of ThreadLocalMap. Weak references can cause memory leaks when the key is returned to NULL during GC and the value is strongly referenced when there is no external reference

ThreadLocalMap -> Entry -> value -> Thread -> Entry -> value -> Thread -> Entry -> value -> Thread -> Entry -> value Memory leaks are bound to occur. What’s more, most of us operate on threads as thread pools.

Is that another memory leak how to fix?

ThreadLocal sets up two levels of safeguards:

  • Key: created as a weak reference object
  • callset().get().remove()Key = null is cleared for value

Summary: The root cause of a threadLocal memory leak is that since ThreadLocalMap has the same lifetime as Thread, a memory leak occurs if the corresponding key is not manually removed, not because of weak references.

Tip: Be timely when using ThreadLocalremove()The habit of

Source code analysis to prevent memory leaks to clean operations

There are two types of cleanup in ThreadLocal:

  • ExpungeStaleEntry () exploratory cleanup
  • CleanSomeSlots () heuristic cleaning

Remove () the source code

public void remove(a) {
    // Get the threadLocals of the current thread binding
    ThreadLocalMap m = getMap(Thread.currentThread());
    // Remove the value of the ThreadLocal object pointed in the current thread when map is not null
    if(m ! =null)
        m.remove(this);
}

private void remove(ThreadLocal
        key) {
    Entry[] tab = table;
    int len = tab.length;
    // Calculate the subscript of the ThreadLocal key
    int i = key.threadLocalHashCode & (len-1);
    // Loop over as long as it is not null
    for(Entry e = tab[i]; e ! =null;
         e = tab[i = nextIndex(i, len)]) {
        // Compare key values, if they are equal
        if (e.get() == key) {
            // Call clear() to clear it up
            e.clear();
            // Perform probe cleaning to clear the node whose key is null
            expungeStaleEntry(i);
            return; }}}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();// Set the table key to null and clear the reference to value
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if(h ! = i) { tab[i] =null;
                // Because the open address method is used, the element to be deleted is one of several conflicting elements
                // move the next element forward
                // 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 get () the source code


public T get(a) {
    // Get the threadLocals of the current thread binding
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // Map is not null
    if(map ! =null) {
        // Query the Entry corresponding to the current ThreadLocal variable instance.
        ThreadLocalMap.Entry e = map.getEntry(this);
        // If not null, get value, return
        if(e ! =null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
            returnresult; }}// Initialize map when it is null
    return setInitialValue();
}

private Entry getEntry(ThreadLocal
        key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];

    // If the corresponding entry exists and ThreadLocal is the key, the result is returned
    if(e ! =null && e.get() == key)
        return e;
    else
        // If not, a linear probe is used to find elements later
        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();// The current ThreadLocal of this entry returns data
        if (k == key)
            return e;
        // The ThreadLocal corresponding to this entry has been reclaimed for exploratory cleaning
        if (k == null)
            expungeStaleEntry(i);
        else
            // point to the next slot and cycle down
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

Copy the code

Set () the source code

private void set(ThreadLocal
        key, Object value) {
    / /... Omit some code
    for (Entry e = tab[i];
		   / /... Omit some code
        // If the key is null, the GC collects data
        if (k == null) {
            // Clean up the data replaceStaleEntry whose key is null by GC
            replaceStaleEntry(key, value, i);
            return; }}/ /... Omit some code
    // Check whether a capacity expansion cleanSomeSlots(I, SZ) heuristic is required
    if(! cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }Copy the code

Looking at the source code calls for the set(), get(), and remove() methods, it’s clear that ThreadLocal does a lot of cleaning to prevent memory leaks

Specific removal process

ExpungeStaleEntry: starts with the node whose ThreadLocal has been reclaimed and deletes the node whose ThreadLocal has been reclaimed


/** * probe clear *@paramStaleSlot is the node position of null *@return* /
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();// Set the table key to null and clear the reference to value
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if(h ! = i) { tab[i] =null;
                // Because the open address method is used, the element to be deleted is one of several conflicting elements
                // move the next element forward
                // 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
  • CleanSomeSlots: Perform multiple cleanup operations from the specified node location with n to control the number of lookups
    • ExpungeStaleEntry returns the location of the next empty node
    • CleanSomeSlots scans again from the next empty node location
    • How many scans can be performed, the first is the n passed in, and from the second is determined by the length of the table
@return */ private Boolean cleanSomeSlots(int I, int I, 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 && um participant et () = = null) {/ / expand scanning control factor set table length n = len; removed = true; I = expungeStaleEntry(I); } } while ((n >>>= 1) ! = 0); return removed; }Copy the code

Summary: You can see that the bottom layer of cleanSomeSlots is still cleared by expungeStaleEntry. However, cleanSomeSlots clears more than expungeStaleEntry

Hash conflict with ThreadLocalMap

ThreadLocal does not have java.util.HashMap as the underlying Map data structure, so a different Hash conflict resolution is required for java.util.HashMap using array + linked list (the linked list has a length converted to a red-black tree) The Hash conflict problem ThreadLocalMap uses linear probing

private void set(ThreadLocal
        key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // Find subscripts in the map based on the hash
    int i = key.threadLocalHashCode & (len-1);
    // Perform linear probe if there is a conflict then operate inside the circulator
    for(Entry e = tab[i]; e ! =null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<? > k = e.get();// The key of the conflicting position is itself
        if (k == key) {
            e.value = value;
            return;
        }
        // If the key is null, the GC collects data
        if (k == null) {
            // Clear the data whose key is GC null
            replaceStaleEntry(key, value, i);
            return; }}// Find an available bucket assignment
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // Check whether capacity expansion is required
    if(! cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }Copy the code

Capacity expansion mechanism of ThreadLocalMap

Related capacity expansion parameters:

/** * Number of capacity items to initialize */
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 current table */
private int size = 0;

/** * Next expansion threshold for table expansion */
private int threshold; // Default to 0

/** * Set capacity expansion threshold formula */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}
Copy the code

The concrete set() method

private void set(ThreadLocal
        key, Object value) {
		/ /... The above code is omitted
      // Check whether capacity expansion is required
      // Condition: The number of entries in the table >= the current capacity expansion threshold when the data is not cleared by the heuristic operation
    if(! cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }Copy the code

We know the basic expansion judgment from the basic parameters, expansion parameters, and expansion judgment in set()

  • When a heuristic purge operation does not purge data
  • >= threshold (16 * 2/3 = 10.6)

But is it really true? Then look at the rehash() method


private void rehash(a) {
    // Probe clean GC elements first
    expungeStaleEntries();
    
    // Use lower threshold for doubling to avoid hysteresis
    
    // The number in the cleared table is greater than 75% to 80% of the threshold for expansion
    // The value of the first expansion is calculated
    // initial: threshold = threshold = len * 2/3; 16 times 2/3 is 10.6
    if (size >= threshold - threshold / 4)
        resize();
}

Copy the code

Summary: The expansion of ThreadLocalMap performed two Null key element cleanings, and the threshold was calculated after each cleanout. Personally, I understand that the main reason for such operation is that it is necessary to move the position of elements during expansion, so as to reduce the movement operation. Note: The Null key element is cleared again when the element is shifted

【 Relevant information 】

  • Memory leak in ThreadLocal? What’s the reason? How to avoid it? – zhihu