Concurrent programming ThreadLocal principle analysis and memory leakage

Basic introduction

What is ThreadLocal? First, we refer to the source code annotation of ThreadLocal

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one has its own, independently initialized copy of the variable.
ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
Translated as:
This class provides thread local variables. These variables are different from other ordinary variables because each thread accessing variables has its own independently initialized variable copy
ThreadLocal instances are usually decorated with static and private, and will be used to store user id or transaction id

definition:
ThreadLocal is a tool class that provides thread local variables. For a shared variable, each thread has its own independent variable copy, which realizes the data isolation between threads, so as to avoid the problem of thread data security

Notice the difference between this and synchronized

Both ThreadLocal and synchronized are used to solve multi-threaded concurrent access. But ThreadLocal is fundamentally different from synchronized. Synchronized is used for data sharing between threads, while ThreadLocal is used for data isolation between threads.

Application scenario analysis

In fact, ThreadLocal is used in the source code of many frameworks. Let me give two examples. You can take a look at the source code

1. The readHolds variable in reentrantreadwritelock inherits ThreadLocal and is used to store the number of times the thread obtains the read lock
2. Some beans in spring (such as RequestContextHolder, TransactionSynchronizationManager and LocaleContextHolder)

Basically, it is to store the context information of thread variables. Let's give you the simplest example

After the user logs in, you need to get the user information anywhere in the project. What will you do if you use ThreadLocal?

  • Store user information before entering the method -- > threadlocal.set ("user")
  • When user information is needed -- > ThreadLocal. Get()
  • After the method is executed, clear threadLocal - > threadLocal. Remove()

Basic introduction

What is ThreadLocal? First, let's refer to the source code annotation of ThreadLocal

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one has its own, independently initialized copy of the variable.
ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
Translated as:
This class provides thread local variables. These variables are different from other ordinary variables because each thread accessing variables has its own independently initialized variable copy
ThreadLocal instances are usually decorated with static and private, and will be used to store user id or transaction id

definition:
ThreadLocal is a tool class that provides thread local variables. For a shared variable, each thread has its own independent variable copy, which realizes the data isolation between threads, so as to avoid the problem of thread data security

Notice the difference between this and synchronized

Both ThreadLocal and synchronized are used to solve multi-threaded concurrent access. However, ThreadLocal is essentially different from synchronized. Synchronized is used for data sharing between threads, while ThreadLocal is used for data isolation between threads.

Application scenario analysis

In fact, ThreadLocal is used in the source code of many frameworks. Let me give two examples. You can take a look at the source code

1. The readHolds variable in reentrantreadwritelock inherits ThreadLocal and is used to store the number of times the thread obtains the read lock
2. Some beans in spring (such as RequestContextHolder, TransactionSynchronizationManager and LocaleContextHolder)

Basically, it is to store the context information of thread variables. Let's give you the simplest example

After the user logs in, you need to get the user information anywhere in the project. What will you do if you use ThreadLocal?

  • Store user information before entering the method -- > threadlocal.set ("user")
  • When user information is needed -- > ThreadLocal. Get()
  • After the method is executed, clear threadLocal - > threadLocal. Remove()

Source code analysis

Data structure of ThreadLocal

You can take a general look at the data structure of ThreadLocal. In fact, each Thread has a ThreadLocalMap,
ThreadLocal is actually the ThreadLocalMap of the operating thread
, the key of ThreadLocalMap is ThreadLocal, and the value is the value to be stored

Let's take a look at the specific process of the set method
You can take these questions with you

  • What is the Hash algorithm of ThreadLocalMap?
  • How are hash conflicts resolved?
  • How is capacity expansion handled?
  • Why design a weak reference?

The following solves these problems one by one according to the source code

First, let's look at the set method

threadLocal.set(T value)

    public void set(T value) {
        //Get the current thread
        Thread t = Thread.currentThread();
        //Get the ThreadLocal.ThreadLocalMap variable in the thread
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //Here, the current threadLocal object is put into the ThreadLocalMap of thread as a key
            //Note that this here is the threadLocal object
            map.set(this, value);
        else
            //Create a new map and put in the values
            createMap(t, value);
    }

Specific process:

  • Get the ThreadLocalMap in the thread. Each thread will have a ThreadLocalMap variable
  • If there is a map, directly put the current ThreadLocal object and the corresponding value into the map
  • If it doesn't exist, create one

createMap(t, value);

Then let's look at the internal processing of the createMap(t, value); method when initializing the map

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
     ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //Create a new entry with an initial capacity of 16
            table = new Entry[INITIAL_CAPACITY];
            //Note the hash algorithm here,
            //firstKey.threadLocalHashCode is actually nextHashCode.getAndAdd(0x61c88647);
            //0x61c88647 is a golden section number, which greatly reduces the probability of hash conflict
            //Then i is the corresponding array subscript position of the current key in the hash table
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            //Set the load factor, which is 2 / 3 of the capacity
            setThreshold(INITIAL_CAPACITY);
        }

You can see from the source code

  • Inside is an Entry array
  • For the hash algorithm of ThreadLocalMap, the increment here is a golden section number, which greatly reduces the hash conflict
  • The load factor is 2 / 3

map.set(this, value);

Let's take a look at the specific implementation of map.set(this, value)

private void set(ThreadLocal<?> key, Object value) {
            //Gets the array created previously
            Entry[] tab = table;
            int len = tab.length;
            //The hash method here is the same as before
            int i = key.threadLocalHashCode & (len-1);
            //Pay attention to the way it is written here
            //1. First find the I subscript Entry e = tab[i] in the array
            //2. If the i subscript is that there are already elements (note that if e == null here, you can directly exit the loop and execute the next step)
            //3. Traverse the array from the i subscript until a = = null is found
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //Only when the element of the current i subscript is! = null will it come in
                //Gets the value of the current key
                ThreadLocal<?> k = e.get();
                //Judge if it is the current key
                if (k == key) {
                    //Update the value directly and return
                    e.value = value;
                    return;
                }
                //If k==null
                //Some students may ask why it is empty here?
                //Because ThreadLocal is a weak reference, it will be recycled by GC, but value is not recycled
                //This will lead to this situation, which will be explained in detail below
                if (k == null) {
                    //Clean up invalid elements and set values
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //Set the element directly when it is not null
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //Returning true here indicates that there are invalid elements
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                //Capacity expansion
                rehash();
        }

Let's sort out the process of set method:

1. Take the hash value and find the corresponding subscript i in the array
2. If the element corresponding to i is not empty, a hash conflict occurs
3. Traverse backward from i to determine the key of the following element
4. If the key is equal to the current key, update the value directly and return
5. If key==null, it means that some weak references have been recycled by GC and invalid elements have been generated. At this time, the invalid elements will be cleaned up and the value will be set
6. If the element corresponding to i is empty, you can directly set the value and judge whether capacity expansion is required, and then end

From this method, we can know how to solve hash conflicts, and then we'll see how to set the value

Replacestateentry method;

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            int slotToExpunge = staleSlot;
            //Traverse forward from the current node to find the first invalid location slotToExpunge
            for (int i = prevIndex(staleSlot, len);
                 //If the element is empty, the loop exits
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            //Traverses backward from the current element
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
                    //After finding the key, first set the value value for e
                    e.value = value;
                    //Then exchange the invalid element with the current element
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //If slotToExpunge == staleSlot, the previous invalid position was not found during the previous forward traversal
                    //Then start cleaning from the current location
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //When the expungeStaleEntry method encounters an empty entry, it returns the i of the empty entry, where len is the length
                    //Continue backward cleaning
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                //If the current element is invalid and there are no invalid elements during the forward scan, the slotToExpunge is updated to the current position
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            //If the key does not exist in the table, just put one in place
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            //If any invalid elements are found, do a clean-up
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

Process sorting:

1. Traverse forward from the current node to find the first invalid position and record it to slotToExpunge
2. Traverse backward from the current element, because the element with key==null was encountered before, so this traversal should try to continue to find elements with equal keys
3. If an element with the same key is found, update the value, exchange the previously found invalid element with the current element, and then clean up from the first invalid position found previously
4. If no element with equal key is found, set key and value directly at the position of the currently invalid element
5. Judge whether it is necessary to clean again according to slotToExpunge

Now we know how to set the value when we find the element with key==null
So how do you clean up elements? Let's take a look

cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

First, take a look at the expungeStaleEntry

//Traverse the cleanup from the current position, clean up the invalid objects, and set the table[i] pointing to the entry to null until the empty entry is swept
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            //It is preferred to clean up the current location
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            Entry e;
            int i;
            //Then traverse back
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //If it is found that it is invalid, clean it up
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                    //rehash, reassign Subscripts
                } 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;
                    }
                }
            }
            //I here is I when e = tab[i]) == null
            return i;
        }

Process combing

1. First clean up the values of the current invalid location, and then size –
2. Start traversing backward and clean up all invalid ones
3. Valid element reassignment subscript
4. When tab[i] == null, end the traversal and return I

In fact, the logic of this piece is relatively clear and concise. Note that the condition returned here is tab[i] == null. This is a linear detection. At this time, in fact, there may be invalid elements behind I, so continue to clean up and have a look

cleanSomeSlots method

		//Continue scanning from i+1 node to clean up invalid elements
        //Note that i is a subscript returned before and cannot be an invalid element, so start with i+1
        //Here n is the length of the array and represents the number of cleanups log n
        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;
        }

Process sorting:

1. Find the i+1 element and judge whether it is an invalid element
2. If yes, call the above expungeStaleEntry method to clean up invalid elements (only before the next element is null)
3. If not, continue to traverse. Note that log will only be traversed n times

This process is called a heuristic cleanup
The heuristic cleanup here and the previous linear probe cleanup can clean up most of the invalid elements

Finally, let's look at the process of capacity expansion

rehash

 private void rehash() {
            //Clean up all invalid elements first (full)
            expungeStaleEntries();
            // Use lower threshold for doubling to avoid hysteresis
            //Capacity expansion when the load is exceeded
            //Because of a clean-up, the size is likely to become smaller.
            //The implementation of ThreadLocalMap here is to lower the threshold to determine whether capacity expansion is required,
            //The default threshold is len*2/3, so the threshold - threshold / 4 here is equivalent to len/2
            if (size >= threshold - threshold / 4)
                resize();
        }

Before capacity expansion, you will clean up all invalid data, and then start capacity expansion. In fact, the capacity expansion process of resize here is similar to that of ordinary arrays

Because the process of get and the code of remove are relatively simple, we won't repeat it here. If you are interested, you can check it yourself~

Why may memory leaks occur and solutions?

Look at the structure of Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

It can be found that ThreadLocal here is a weak reference. When will this weak reference be cleaned up?
When there is no external strong reference to reference this ThreadLocal instance, GC will be cleaned up,
Give you the simplest example of a memory leak

 public static void main(String[] args) {
        //Strong reference
        ThreadLocal<String> local = new ThreadLocal<>();
        local.set("aaa");
        //Strong references released
        local = null;
        //At this time, the GC finds that the strong reference local of ThreadLocal is gone, so it will clean up the weak reference of ThreadLocal
        System.gc();
        //At this time, if the thread is still running (such as the thread in the thread pool), a memory leak occurs
    }

In fact, when we normally use ThreadLocal, there are fewer memory leaks, but this is possible,
Now when we talk about ThreadLocal outside, we are all talking about the tiger and telling the wrong story. In fact, there is no need. However, we still suggest that you use it with the remove method to be prepared~
Only when we know the cause of ThreadLoca memory leak can we make better use of it

Some students here will ask, why do you have to design such a weak reference because it is so troublesome? Can't the key here be directly referenced by strong reference?

Here we discuss it in two cases:

key uses strong reference: after local ==null in the above example, local breaks the strong reference to ThreadLocal instance, but ThreadLocalMap still holds the strong reference to ThreadLocal. If it is not deleted manually, ThreadLocal will not be recycled, resulting in Entry memory leakage.
key uses weak reference: after local ==null in the above example, local breaks the strong reference to ThreadLocal instance. Since ThreadLocalMap holds the weak reference to ThreadLocal, ThreadLocal will be recycled even if it is not deleted manually. value will be cleared the next time ThreadLocalMap calls set, get and remove.

Comparing the two cases, we can find that the life cycle of ThreadLocalMap is as long as that of Thread. If the corresponding key is not manually deleted, memory leakage will occur, but using weak references can provide an additional layer of protection

Therefore, the root cause of ThreadLocal memory leak is: because the life cycle of ThreadLocalMap is as long as Thread, if the corresponding key is not manually deleted, it will lead to memory leak, not weak reference.

summary

The conclusions are all above. You are welcome to point out any questions or opinions, make progress together and encourage each other~

Tags: Java jvm Concurrent Programming thread Memory Leak

Posted on Thu, 11 Nov 2021 17:16:22 -0500 by PHPNewbie55