This article is part of the Android Handler mechanism series. If you want to learn more about the Android Handler mechanism, please check out the Android Handler Mechanism overview.

preface

To understand Android Handle, we first need to understand ThreadLocal, which we can all guess by its literal meaning. Thread-local variables. So what are the benefits of storing variables locally? How does that work? Let’s take a look at how ThreadLocal works.

A brief introduction to ThreadLocal

This class provides thread-local variables. These variables differ from their normal variables in that each thread accessing its own local variable has its own, independently initialized copy. This variable is typically a private static field associated with a thread, such as an ID or a transaction ID. After you read the introduction, it is possible that you still do not understand the main role of its main, simple draw a picture to help you understand.

With ThreadLocal, each thread can retrieve its own internal private variables, which might make people think that there is no truth to it. “How do I know if you are right or wrong?” , let’s look at the following code through a detailed introduction of specific examples.

Private static ThreadLocal<String> mThreadLocal = new ThreadLocal<>(); public static void main(String[] args) { mThreadLocal.set("Main" thread);
        new Thread(new A()).start();
        new Thread(new B()).start();
        System.out.println(mThreadLocal.get());
    }

    static class A implements Runnable {

        @Override
        public void run() {
            mThreadLocal.set("Thread A");
            System.out.println(mThreadLocal.get());
        }
    }

    static class B implements Runnable {

        @Override
        public void run() {
            mThreadLocal.set("Thread B"); System.out.println(mThreadLocal.get()); }}}Copy the code

In the appeal code, we set the value of mThreadLocal in the main thread to “thread main”, in thread A to” thread A “, in thread B to “thread B”, and run the program to print the result as shown below:

Main Thread A Thread BCopy the code

As you can see from the above results, although the same variable mThreadLocal is accessed from different threads, they get different values from ThreadLocl. Now that we know how ThreadLocal works, let’s take a look at how it works.

ThreadLocal principle

To get you up to speed on how ThreadLocal works, here’s how ThreadLocal works:

In the figure above, we can see that the entire use of ThreadLocal involves the use of ThreadLocalMap in the thread, even though we call threadlocal.set (value) externally, However, it is essentially a set(key,value) method in a ThreadLocalMap, so we can guess from this situation that the get method is also a ThreadLocalMap method. Let’s take a look at the implementation of the set and GET methods in ThreadLocal and the structure of ThreadLocalMap.

ThreadLocal’s set method

When using a ThreadLocal, we call the ThreadLocal set(T value) method to set a private variable in the thread

    public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // Get the thread's LocalMapif(map ! = null) map.set(this, value); // Set key-> current ThreadLocal object. Value -> Specifies the current assigned valueelsecreateMap(t, value); // Create a new ThreadLocalMap and set the value}Copy the code

When the set(T value) method is called, the ThreadLocalMap in the current thread will be obtained internally. If the value is not empty, the set method of ThreadLocalMap will be called (key is the current ThreadLocal object, Value is the current assigned value. Instead, let the current thread create a new ThreadLocalMap and set the values. The getMap() and createMap() methods code as follows:

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

We’ve seen this in a nutshell with ThreadLocalMap’s set() method. All data operations in a ThreadLocal are related to a ThreadLocalMap, so let’s look at the code for ThreadLocalMap.

Internal structure of ThreadLocalMap

ThreadLocalMap is a static inner class of ThreadLocal. ThreadLocalMap is a custom hash map created to maintain thread private values. Threads of the private data is very big and long service life (actually think about it, why do you want to store the data, the first is for the sake of the common data into the thread to improve the speed of access, the second is that if the data is very big, to avoid the frequent data created, not only solved the problem of storage space, also reduces unnecessary IO consumption).

ThreadLocalMap code is as follows:

Static class ThreadLocalMap {// Stores data as Entry and key as WeakReference Static Class Entry extends WeakReference<ThreadLocal<? >> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<? > k, Object v) { super(k); value = v; }} //table initial capacity private static final int INITIAL_CAPACITY = 16; //table is used to store data private Entry[] table; // Load factor, used for array capacity expansion private int threshold; // Default to 0 // Load factor. By Default, the load factor is 2/3 of the current array lengthsetThreshold(int len) { threshold = len * 2 / 3; } // When Entry data is put in for the first time, initialize the array length, define the expansion threshold, ThreadLocalMap(ThreadLocal<? > firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; / / initialize array length is 16 int I = firstKey. ThreadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1;setThreshold(INITIAL_CAPACITY); // The threshold is 2/3 of the default length of the current array}Copy the code

As you can see from the code, ThreadLocalMap is officially declared to be a hash table, but it is different from the internal structure of a HashMap. ThreadLocalMap maintains only Entry[] tables, arrays. The key corresponding to the Entry entity is a weak reference (why weak reference is used below). When data is put in for the first time, the array length is initialized (16) and the array expansion threshold is defined (2/3 of the current default array length).

ThreadLocalMap’s set() method

 private void set(ThreadLocal<? > key, Object value) {// Calculate the position Entry according to the hash value [] TAB = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); // Check whether there is data in the current position. If the key value is the same, replace it. If the key value is different, find a space to put data.for(Entry e = tab[i]; e ! = null; E = TAB [I = nextIndex(I, len)]) {ThreadLocal<? > k = e.get(); // Check whether the key value is the same, if it is directly overwritten (first case)if (k == key) {
                    e.value = value;
                    return; } // If the current Entry object corresponds to a null Key, empty all data with a null Key (the second case)if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return; }} // Add TAB [I] = new Entry(key, value); int sz = ++size;if(! CleanSomeSlots (I, sz) &&sZ >= threshold)// If the current array reaches the threshold, expand it.rehash(a); }Copy the code

It is difficult to understand directly through the code, so the set method is directly divided into three steps. Here we will explain the three steps respectively through the diagram and code.

In the first case, the Key value is the same

If the key value of the Entry corresponding to the current position is the same as that of the newly added Entry in the current array, the operation is directly overwritten. The specific situation is shown in the figure below

If the current array. If the key value is the same, the internal operation of ThreadLocal is overridden directly. This situation can not be described enough.

In the second case, if the Key value of the Entry corresponding to the current position is NULL

The second case is a bit more complicated, and I’ll show you the diagram and then the code.

We can see that from the picture. When we add a new Entry(key=19,value =200,index = 3), the array already contains the old Entry(key= NULL,value =19). In this case, the method internally assigns all the values of the new Entry to the old Entry. All entries with a null key in the array are set to NULL (large yellow data). In the source code, the replaceStaleEntry method is used when data exists in the corresponding position of the new Entry and the key is null. The specific code is as follows:

private void replaceStaleEntry(ThreadLocal<? > key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; Int slotToExpunge = staleSlot; // Go ahead and find the first Entry that expired (key is empty)for(int i = prevIndex(staleSlot, len); (e = tab[i]) ! = null; i = prevIndex(i, len))if(LLDB () == null)// Check whether the reference is empty. If the reference is empty, the position where the first expired Entry is erased is slotToExpunge = I. // Go back and find the last expired Entry(key is empty),for(int i = nextIndex(staleSlot, len); (e = TAB [I])! = null; i = nextIndex(i, len)) { ThreadLocal<? > k = e.get(); // If the key value is the same as the key value when looking for it later. So reassign.if(k == key) {// Assign to the location of the previous passed staleSlot e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // If no Entry is out of date, the current location is recorded.if(slotToExpunge == staleSlot) slotToExpunge = i; CleanSomeSlots (expungeStaleEntry(slotToExpunge), len);return; } // If no Entry is out of date, and key= null, then the current position is kept (key==null)if(k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // Set the data corresponding to the current null key to NULL and create a new Entry on that position TAB [staleSlot]. Value = null; tab[staleSlot] = new Entry(key, value); // If the slotToExpunge location does not contain an Entry with an expired key, and the first Entry with an expired key is found before staleSlot, then all data with a null key under slotToExpunge is clearedif(slotToExpunge ! = staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }Copy the code

The above code looks complicated, but if you comb through it carefully, you will find that in fact, this method mainly evaluates four cases, as shown in the chart below:

We already know that the replaceStaleEntry method cleans key== NULL data internally, and that the specific methods are related to the expungeStaleEntry() and cleanSomeSlots() methods, so let’s examine them. Look at the implementation.

ExpungeStaleEntry () method

private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; StaleSlot = null TAB [staleSlot]. Value = null; tab[staleSlot] = null; size--; Entry e; int i; // Look later.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{// If the key is not null, but the threadLocalHashCode corresponding to the key changes, // calculates the position and places the element in the new position. int h = k.threadLocalHashCode & (len - 1);if(h ! = i) { tab[i] = null;while(tab[h] ! = null) h = nextIndex(h, len); tab[h] = e; }}}returni; // return the last TAB [I])! = null position}Copy the code

The expungeStaleEntry () method does three things. First, it sets the data corresponding to the staleSlot location to NULL. Second, it deletes and deletes the data corresponding to the staleSlot location key = NULL. Third, if the key is not null, but the key’s corresponding threadLocalHashCode changes, calculate the changed position and put the element in the new position.

CleanSomeSlots () method

    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);returnremoved; // If any expired data is deleted, returntrueAnd vice versafalse
        }
Copy the code

Once you know the expungeStaleEntry () method, it’s easy to understand the cleanSomeSlots () method. The first parameter indicates the position to start the scan, and the second parameter indicates the length of the scan. This is obvious from the code. Delete key==null at all locations.

In the third case, the current position is null

In order to make it easier for you to understand the situation of clearing up and down data, I did not recalculate the position (please note!!).

I posted the code directly to make it easier for you to avoid looking at it unnecessarily. Here’s the code.

tab[i] = new Entry(key, value);
int sz = ++size;
if(! cleanSomeSlots(i, sz) && sz >= threshold)rehash(a);Copy the code

INITIAL_CAPACITY *2/3 (INITIAL_CAPACITY = 16); INITIAL_CAPACITY *2/3 (INITIAL_CAPACITY = 16); INITIAL_CAPACITY *2/3 (INITIAL_CAPACITY = 16); If you get to the point where you recalculate the data. The code for the rehash() method is as follows:

 private void rehash() {
         expungeStaleEntries();

         // Use lower threshold for doubling to avoid hysteresis
         if(size >= threshold - threshold / 4) resize(); } // Empty all data where key==null private voidexpungeStaleEntries() {
         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); }} // recalculate key! =null data. The new array is double the previous length private voidresizeEntry[] oldTab = table; () {// Double the size of the original array. int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; // recalculate the positionfor (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++; }}} // Recalculate the threshold (load factor) to 2/3 of the enlarged array lengthsetThreshold(newLen);
            size = count;
            table = newTab;
        }
Copy the code

I’ve listed all the methods involved inside Rehash. As you can see, expansion is performed when adding data, and if expansion is required, all key== NULL data will be removed (i.e., the expungeStaleEntry () method is called, which has already been introduced but will not be described). The position in the data is also recalculated.

ThreadLocal’s get() method

Now that we know about the set() method of a ThreadLocal, let’s look at how to retrieve data from a ThreadLocal:

  public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // Get the Map from the threadif(map ! Threadlocalmap. Entry e = map.getentry (this);if(e ! = null) { @SuppressWarnings("unchecked")
                T result = (T)e.value;
                returnresult; }} // If ThreadLocalMap is empty, create a new ThreadLocalMapreturn setInitialValue();
    }
Copy the code

The ThreadLocal get method is a simple one: get the ThreadLocalMap object from the current thread, create it if it doesn’t exist, and get the corresponding data based on the current key(the current ThreadLocal object). The getEntry () method area of ThreadLocalMap is called internally to get the data, and we continue to look at the getEntry () method.

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);
        }
Copy the code

The getEntry () method is also very simple inside, which is to find the corresponding position in the array based on the current key hash position. If so, the data is directly put back. If not, the getEntryAfterMiss () method is called.

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 if the key is the samereturn e;
                if(k ==null)// If key==null, clear all data under the current position. expungeStaleEntry(i);else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            returnnull; // No data returns null}Copy the code

The expungeStaleEntry () method is also called to remove all key==null data from the array. This means that now either the set () or get() methods of ThreadLocal are called to clean up the data with key==null.

ThreadLocal memory leak problem

After exploring the entire ThreadLocal mechanism, I’m sure you’ll be wondering why ThreadLocalMap uses weak references as keys. This issue involves Java’s recycling mechanism.

Why weak references

Determining whether an object needs to be recycled in Java is all about references. References are divided into four classes in Java.

  • Strong references: Garbage collector will never reclaim Object obj = new Object() as long as the reference exists; A strong reference to a new Object will be released only when the obj reference is released.
  • Soft references are used to describe objects that are still available, but not required, to be recycled for a second time before the system runs out of memory. (SoftReference)
  • Weak references: Also used to describe non-essential objects, but weaker than soft references. Objects associated with weak references only survive until the next garbage collection occurs, when the garbage collector works to reclaim the weakly referenced objects regardless of whether there is currently enough memory. (WeakReference)
  • Virtual references: Also known as ghost references, this is the weakest kind of relationship. The existence of a reference to an object does not affect its lifetime at all, nor can a virtual reference be used to obtain an instance object.

If a key uses a strong reference to a ThreadLocal, then the object that references a ThreadLocal is reclaimed, but the ThreadLocalMap still contains a strong reference to a ThreadLocal. ThreadLocal is not recycled, resulting in a memory leak.

Problems with weak references

Now that we know why weak references are used as keys in ThreadLocalMap, another problem arises when we call ThreadLocal’s set () or get() methods to clean up data where key==null is used. Why do we need to clear entries with key==null?

There are two reasons why the Entry whose key==null is cleared, as follows:

  • As we already know, ThreadLocalMap uses a weak reference to a ThreadLocal as its key. That is, if a ThreadLocal has no external strong reference to it, the ThreadLocal will be reclaimed during GC. This will result in null key entries in the ThreadLocalMap, and there will be no way to access the values of these null key entries.
  • If the current thread does not terminate, the value of each Entry with a null key will always have a strong reference chain: If Thread Ref (current Thread reference) -> Thread -> ThreadLocalMap -> Entry -> value, these entries will never be collected, causing a memory leak.

From the above analysis, we can see that ThreadLocalMap has been designed with both of these situations in mind and some safeguards in place. Call ThreadLocal get(),set(), and remove() all entries with null keys in ThreadLocalMap are removed.

Precautions for using ThreadLocal

ThreadLocal takes memory leaks into account for us, but adds some safeguards. However, in practical use, we still need to be careful to avoid the following two situations, which can still cause memory leaks.

Avoid static ThreadLocal

Static ThreadLocal extends the lifetime of ThreadLocal and may cause memory leaks. The reason is that the Java virtual machine allocates memory for static variables during class loading. The life cycle of static variables depends on the life cycle of the class, meaning that static variables are destroyed and free up memory only when the class is unloaded. The end of the life cycle of a class is related to three conditions.

  1. All instances of the class have been reclaimed, meaning that there are no instances of the class in the Java heap.
  2. The ClassLoader that loaded the class has been reclaimed.
  3. The java.lang.Class object corresponding to this Class is not referenced anywhere, and there are no methods to access the Class through reflection anywhere.

Allocation uses ThreadLocal without calling the get(),set(), and remove() methods

Get (), set(), and remove() methods are not called after the first call to ThreadLocal. That is, there is now only one data in ThreadLocalMap. So if the thread calling a ThreadLocal never terminates, there will always be a strong reference chain even if the ThreadLocal has been set to null: Thread Ref (current Thread reference) -> ThreadLocalMap -> Entry -> value

conclusion

  • ThreadLocal essentially operates on a thread’s ThreadLocalMap to store local thread variables
  • ThreadLocalMap stores data as an array, with key(weak reference) pointing to the current ThreadLocal object and value being the set value
  • ThreadLocal handles memory leaks by calling ThreadLocal’s get(),set(), and remove() methods to remove all entries with null keys from the ThreadLocalMap
  • When using a ThreadLocal, you need to avoid using static ThreadLocal. It is important to determine whether you need to manually clean entries in ThreadLocalMap with key==null based on the lifetime of the current thread.