1. Introduction

ThreadLocal is also a frequently used class and is often seen in frameworks such as Spring.

In many articles on source code analysis of ThreadLocal, one question is often asked: Does ThreadLocal have a memory leak?

Many articles on this relatively vague, often let people read the brain or confused, I also have this confusion. Therefore, I found some time to discuss with my friends, and finally have a certain understanding of this problem. Here is a record and share, hoping to help friends who have the same confusion. Of course, if there is any improper understanding of the place is also welcome to correct.

With that out of the way, let’s start with an application scenario for ThreadLocal.

2. Application scenarios

ThreadLocal can be used in a number of scenarios, but one simple example is single sign-on interception.

This is to determine whether the user is logged in before processing an HTTP request:

  • If you have not logged in, the login page is displayed.
  • If you have logged in, obtain and save the login information.

Define a UserInfoHolder class to hold user login information internally with ThreadLocal, as shown in the following example:

public class UserInfoHolder {
    private static final ThreadLocal<Map<String, String>> USER_INFO_THREAD_LOCAL = new ThreadLocal<>();

    public static void set(Map<String, String> map) {
        USER_INFO_THREAD_LOCAL.set(map);
    }
    
    public static Map<String, String> get(a) {
        return USER_INFO_THREAD_LOCAL.get();
    }
    
    public static void clear(a) {
        USER_INFO_THREAD_LOCAL.remove();
    }
    
    // ...
}
Copy the code

The UserInfoHolder allows you to store and obtain user login information for use in your business.

Spring in the project, if we want to deal with an HTTP request before or after doing some additional processing, usually define a class inherits HandlerInterceptorAdapter, some methods and then rewrite it. Here is an example (for reference only, some code has been omitted) :

public class LoginInterceptor extends HandlerInterceptorAdapter {

    // ...
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        
        // ...

        // Before the request is executed, obtain and save the user login information
        Map<String, String> userInfoMap = getUserInfo();

  UserInfoHolder.set(userInfoMap);

  return true;
    }

    @Override
 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // After the request is executed, the user information is clearedUserInfoHolder.clear(); }}Copy the code

In this case, we get the user’s information before processing a request, and clear the user’s information after processing the request. Some of you have seen code like this in frameworks or in your own projects.

Let’s dive into the bowels of ThreadLocal to see what these methods do and how they relate to memory leaks.

3. Source code analysis

3.1 class signature

Start at the beginning, which is the class signature:

public class ThreadLocal<T> {}Copy the code

You can see that it is a normal class with no interface and no superclass inheritance.

3.2 the constructor

ThreadLocal has only one no-parameter constructor:

public ThreadLocal(a) {}Copy the code

In addition, JDK 1.8 introduced a static method withInitial that uses lambda expressions as follows:

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}
Copy the code

This method also initializes an object and is similar to a constructor.

3.3 ThreadLocalMap

3.3.1 Main code

ThreadLocalMap is an internally nested class of ThreadLocal.

Since most of the operations of ThreadLocal are actually implemented through ThreadLocalMap methods, let’s examine the main code of ThreadLocalMap:

public class ThreadLocal<T> {
    // Generate the hash code for ThreadLocal, which is used to calculate the position in the Entry array
    private final int threadLocalHashCode = nextHashCode();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode(a) {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    // ...
    
    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 2
        private static final int INITIAL_CAPACITY = 16;
    
        // An array to store data
        private Entry[] table;

        // Number of entries in the table
        private int size = 0;

        // Capacity expansion threshold
        private int threshold; // Default to 0
    
        // Set the capacity expansion threshold
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }    
    
        // Add the constructor used by the element for the first timeThreadLocalMap(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

The internal structure of ThreadLocalMap is similar to that of HashMap.

Both are arrays of key-value pairs, and hash collisions are handled differently, resulting in some structural differences:

  1. HashMap is a “linked list” method used to handle hash conflicts. That is, to pull out a linked list when a conflict occurs, and JDK 1.8 further introduces red-black tree optimizations.
  2. ThreadLocalMap uses “linear probing” in “open addressing”. That is, when a location conflicts, the current location is searched backward until a free location is found.

The rest is similar.

3.3.2 Precautions

  • A weak reference

It’s worth noting that ThreadLocalMap’s Entry inherits the WeakReference class, which is the WeakReference type.

Following up the parent class of Entry, it can be seen that ThreadLocal is finally assigned to the referent property of WeakReference’s parent class Reference. That is, an Entry can be thought of as holding references to two objects: a “weak reference” of type ThreadLocal and a “strong reference” of type Object, where ThreadLocal is the key and Object is the value. As shown in the figure:



Some of the “memory leaks” that ThreadLocal can cause in some cases are related to this “weak reference”, which will be discussed later.

  • addressing

The Entry key is of type ThreadLocal. How does it hash in the array?

ThreadLocal has a threadLocalHashCode variable that is incremented with a fixed value HASH_INCREMENT (0x61C88647) each time a ThreadLocal object is created. This number seems to be related to the Golden section and Fibonacci number, but that is not the point, interested friends can go to in-depth research, here we know its purpose on the line. The purpose of the hash algorithm is similar to that of the HashMap, which is to make the hash more uniform.

Let’s examine the main method implementations of ThreadLocal.

3.4 Main Methods

ThreadLocal has three main methods: set, get, and remove, which are described below.

3.4.1 track set method

  • Set method: Add/update Entry
public void set(T value) {
    // Get the current thread
    Thread t = Thread.currentThread();
    // Get ThreadLocalMap from Thread
    ThreadLocalMap map = getMap(t);

    if(map ! =null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
Copy the code

ThreadLocals is a reference to a ThreadLocalMap held by a Thread, which is null by default:

public class Thread implements Runnable {
    // Other code...
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
Copy the code
  • Execute the process

If the ThreadLocalMap obtained from the current Thread is empty, the attribute is not initialized. Perform createMap initialization:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Copy the code

If it already exists, call ThreadLocalMap’s set method:

private void set(ThreadLocal
         key, Object value) {    
    Entry[] tab = table;
    int len = tab.length;
    // 1. Calculate the index I of the key in the array
    int i = key.threadLocalHashCode & (len-1);
    
    // 1.1 If array subscript I has elements
    // Check whether the Entry of position I is empty; If it is not empty, the array is traversed backwards from I
    for(Entry e = tab[i]; e ! =null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<? > k = e.get();// The element with index I is the element to be searched, and the new value overwrites the old value
        if (k == key) {
            e.value = value;
            return;
        }
        
        // The element with index I is not the element to be searched, and the Entry Key in that position is already null
        // A null Key indicates that the Entry has expired. In this case, replace the expired value with a new value
        if (k == null) {
            // Replace the expired Entry,
            replaceStaleEntry(key, value, i);
            return; }}// 1.2 If the array subscript I is empty, place the element to be stored at I
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // If expired entries are not cleared and the size of the array reaches the threshold, the rehash operation is performed
    if(! cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }Copy the code

First, summarize the main process of set method:

First compute the array subscript of the key based on its threadLocalHashCode:

  1. If the array subscript Entry is not empty, there are already elements at that location. Because of the possibility of hash conflicts, the element at this position may not be the element to be found, so traverse the array to compare

    1. If an Entry equal to the current key is found, it replaces the old one with the new one, and returns.
    2. If an Entry is not empty but the key of the Entry is empty during the traversal, some clearing work is performed.
  2. If the array subscript Entry is empty, place the element directly there, expanding if necessary.
  • ReplaceStaleEntry: Replaces expired values and cleans up some expired entries
private void replaceStaleEntry(ThreadLocal<? > key, Object value,int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    
    // Start from staleSlot, and update slotToExpunge if an expired slot (Entry key is empty) is encountered
    // Stop traversal until Entry is empty
    int slotToExpunge = staleSlot;
    for (inti = prevIndex(staleSlot, len); (e = tab[i]) ! =null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
    
    // Start from staleSlot and iterate backwards. If an Entry equal to the current key is encountered, update the old value and swap the two
    // The purpose is to put it where it "should" be
    for (inti = nextIndex(staleSlot, len); (e = tab[i]) ! =null; i = nextIndex(i, len)) { ThreadLocal<? > k = e.get();if (k == key) {
            // Update the old value
            e.value = value;
            
            / / in position
            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
    // If the key is not found, the Entry does not exist before
    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

The main execution process of replaceStaleEntry is as follows:

  1. The array is traversed forward from staleSlot until the traversal stops when Entry is empty. The main purpose of this step is to find the slotToExpunge array subscript of the expired Entry in front of staleSlot.
  2. The array is traversed backwards from staleSlot

    1. If the key of an Entry is equal to the given key, the Entry is swapped with the Entry subscript of staleSlot. The goal is to put the new Entry where it “should” be.
    2. If no equal key can be found, the Entry corresponding to the key is not in the array. Place the new value in the staleSlot position. This operation is essentially “linear detection” for hash collisions: when one position is occupied, the next position is explored backwards.
  3. If an expired Entry exists in front of staleSlot, the staleSlot is cleared.

PS: The “should” position of an Entry is calculated from the mod of the threadLocalHashCode of the key and the length of the array, i.e. K.htreadlocalhashcode& (len-1), or the position after the hash conflict. This is just for the sake of description.

  • ExpungeStaleEntry: Clears expired entries
// staleSlot indicates an expired slot (the subscript of the Entry array)
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 1. Set the Entry for the given position to NULL
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
    
    // Rehash until we encounter null
    Entry e;
    int i;
    // go through the number group
    for(i = nextIndex(staleSlot, len); (e = tab[i]) ! =null;
         i = nextIndex(i, len)) {
        // Get the Entry keyThreadLocal<? > k = e.get();if (k == null) {
            // If the key is null, the Entry expires and is empty
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // If the key is not empty, the Entry has not expired
            // Calculate the position of the key. If the Entry is not where it "should" be, move it to where it "should" be
            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

What does this method do?

  1. Clears entries for a given location
  2. The array is traversed backwards from the next one at a given position

    1. If Entry is null, the traversal ends
    2. If an Entry with an empty key (that is, an expired one) is encountered, the Entry is empty
    3. If an Entry with a non-empty key is found, and the Entry is calculated to be not where it “should” be, move it to where it “should” be
  3. Returns the index subscript after staleSlot with null Entry
  • CleanSomeSlots: cleanSomeSlots
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 the Entry is not empty and the key is empty, the Entry expires
        if(e ! =null && e.get() == null) {
            n = len;
            removed = true;
            // Clears consecutive expired entries after I until Entry is null, and returns the subscript of this Entryi = expungeStaleEntry(i); }}while ( (n >>>= 1) != 0);
    return removed;
}
Copy the code

What does this method do? The array is scanned starting from the next Entry at a given location, and if an Entry with an empty key (expired) is encountered, that location and the expired slot after it are cleaned.

It is worth noting that this method loops log(n) times. Since this method is called inside the set method, when new/updated:

  1. If not scanned and cleaned, the set method executes quickly, but there are some garbage (expired entries).
  2. If the scan is cleaned every time, there is no garbage, but the insert performance is reduced to O(n).

Therefore, the number of entries is a balancing strategy: if the number of entries is small, it will be cleaned fewer times. When the array is large, we clean it up a few more times.

  • Rehash: Adjusts the Entry array
private void rehash(a) {
    // Clear the array of expired entries
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

// Clean up the entire Entry array from scratch
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

The main functions of this method:

  1. Clears expired entries from the array
  2. If the number of entries is greater than or equal to 3/4 of the threshold, run the resize method to expand the capacity
  • Resize: Expand the Entry array
/** * Double the capacity of the table. */
private void resize(a) {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2; // The new length is twice the old length
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    
    // Iterate over the old Entry array and move the values from the array to the new array
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if(e ! =null) { ThreadLocal<? > k = e.get();// If the Entry key has expired, the Entry is cleared
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // Calculates the position in the new array
                int h = k.threadLocalHashCode & (newLen - 1);
                // Hash collision, linear probe next position
                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

The function of this method is to expand the Entry array. The main process is as follows:

  1. Create a new array twice as long as the original array.
  2. Iterates through the elements of the old array starting at index 0

    1. If the element is expired (key is empty), value is also empty
    2. Moves an unexpired element to the new array

3.4.2 the get method

After analyzing the set method, it is relatively easy to look at the GET method.

  • Get: Gets the Entry corresponding to a ThreadLocal
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; }}return setInitialValue();
}
Copy the code

The get method first gets the ThreadLocalMap of the current thread and determines:

  1. If the Map already exists, the value is obtained from the Map
  2. If the Map does not exist or the value obtained in the Map is empty, run the setInitialValue method
  • SetInitialValue method: Gets/sets the initial value
private T setInitialValue(a) {
    // Get the initial value
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if(map ! =null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue(a) {
    return null;
}
Copy the code

The initial value is taken, which defaults to null (this method is protected and can be initialized by subclasses).

  1. If Thread’s ThreadLocalMap is already initialized, the initial value is saved to the Map
  2. Otherwise, create a ThreadLocalMap
  3. Return initial value

Except for the initial value, the logic is the same as for the set method, which is not described here.

PS: You can see that the initial value is lazily initialized.

  • GetEntry: Obtains the Entry corresponding to the given key from the Entry array
private Entry getEntry(ThreadLocal
         key) {
    // Compute the subscript
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // Lookup hit
    if(e ! =null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

// Key failed to hit
private Entry getEntryAfterMiss(ThreadLocal<? > key,int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    
    // go through the number group
    while(e ! =null) { ThreadLocal<? > k = e.get();if (k == key)
            return e; // Is the key to find, return
        if (k == null)
            expungeStaleEntry(i); // Entry has expired. Clear the Entry
        else
            i = nextIndex(i, len); // iterate backwards
        e = tab[i];
    }
    return null;
}
Copy the code

Rule 3.4.3 the remove method

  • Remove method: Removes the Entry corresponding to ThreadLocal
public void remove(a) {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if(m ! =null)
        m.remove(this);
}
Copy the code

This calls the remove method of ThreadLocalMap:

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

Where E. clear calls the clear method of Reference, the parent class of Entry:

public void clear(a) {
    this.referent = null;
}
Copy the code

Essentially, the Entry key is left blank.

The main execution flow of the remove method is as follows:

  1. Gets the ThreadLocalMap of the current thread
  2. Use the current ThreadLocal as the key, search for the corresponding Entry in the Map, and empty the key of the Entry
  3. Empty the entries corresponding to this ThreadLocal and iterate backwards through the cleanup of the array of entries, the expungeStaleEntry method, which has been analyzed previously and won’t be described here.

3.4.4 Summary of main methods

The main ThreadLocal methods set, GET, and remove have been analyzed previously, so here’s a quick summary.

  • Set method

    • An Entry consisting of the current ThreadLocal as the key and the new Object as the value is put into the ThreadLocalMap, which is an Entry array.
    • After calculating the position of the Entry

      • If the slot is empty, put it right here; Clear some expired entries and expand the capacity if necessary.
      • When a hash conflict is encountered, linear probing looks back for empty or expired slots in the array and replaces them with new values.
  • The get method

    • With the current ThreadLocal as the key, search for the value of the corresponding Entry from the Entry array

      • If ThreadLocalMap is not initialized, it is initialized with the given initial value
      • If ThreadLocalMap is initialized, look for the key from the Entry array
  • Remove method: Use the current ThreadLocal as the key to remove the corresponding Entry from the Entry array, and then remove the expired entries after this position

The methods are few, but slightly convoluted, performing some extra cleanup operations in addition to doing their own functions.

After analyzing the source code for these methods, let’s take a look at memory leaks.

4. Memory leak analysis

First, ThreadLocal is usually used as a member variable or as a static variable (that is, shared), as in the example in the previous application scenario. Since local variables are already inside the same thread, there is no need to use ThreadLocal.

Thread, ThreadLocal, ThreadLocalMap, and Entry classes are shown in the JVM memory diagram.



Brief description:

  • When a Thread is running, there is a stack frame for the current Thread, which holds a strong reference to ThreadLocalMap.

  • The class of a ThreadLocal holds a strong reference to a ThreadLocal; At the same time, the Entry in ThreadLocalMap holds a weak reference to ThreadLocal.

4.1 a scene

If the method completes and the Thread dies normally, the ThreadLocalMap reference to the Thread will be disconnected, as shown in the figure below:



Weak references are also broken later when GC occurs, and the entire ThreadLocalMap is reclaimed, with no memory leak.

4.2 scenario 2

What about threads in a thread pool? The thread is always alive. After GC, the ThreadLocal reference held by the Entry is disconnected, and the key of the Entry is empty, but the value is not, as shown in the figure:



In this case, if there is no action to clean up the Entry array such as remove or get, the Object held by the value of the Entry will not be reclaimed. This creates a memory leak.

This can easily be avoided by simply executing the remove method.

5. Summary

This article looked at the main method implementations of ThreadLocal and looked at scenarios where it might have memory leaks.

  1. ThreadLocal is primarily used by the current thread to save a “copy” of a shared variable. A common scenario is single sign-on to save a user’s login information.
  2. ThreadLocal stores data in a ThreadLocalMap, which is an array of entries and is structured somewhat like a HashMap.
  3. Improper use of ThreadLocal can cause memory leaks. The way to avoid a memory leak is to execute ThreadLocal’s remove method before the method call ends.