In the operating system, we have learned the difference between process and thread. Officially speaking, the process is the smallest unit of resources allocated by the operating system, and the thread is the smallest unit of scheduling by the operating system. Thread is attached to the process, a process must contain at least one thread, process and process are mutually independent, but each thread of a process is to share process resources.

If we want to maintain private data for each thread, there are two ways to do it

  1. A variable table is maintained in the process, and each thread has access to assignments
  2. Each thread maintains a separate variable table and only has access to its own private variable table

The first method has an obvious disadvantage, that is, multi-thread sharing problem. How to ensure synchronization when multi-thread accesses the variable table at the same time, and how to minimize the performance loss caused by synchronization are all problems that need to be considered. In the second way, since each thread’s private variable table is independent, there is no synchronization problem. ThreadLocal is implemented in the second way

usage

  • Create a ThreadLocal. ThreadLocal supports generics, so you can create a ThreadLocal for any object. There are two ways to create a ThreadLocal, either by directly new a ThreadLocal object or by calling the static withInitial method

    // The first way
    ThreadLocal<String> local1 = new ThreadLocal<>();
    // The second way
    ThreadLocal<String> local2 = ThreadLocal.withInitial(new Supplier<String>() {
            @Override
            public String get(a) {
                return "initial value2"; }});Copy the code

    WithInitial provides an initial value for ThreadLocal. When we print the values of local1 and local2, the first is null, and the second is initial value2

    In addition to the withInitial method, there is another way to set the initial value of ThreadLocal: the anonymous inner class

    ThreadLocal<String> local1 = new ThreadLocal<String>(){
            @Override
            protected String initialValue(a) {
                return "initial value1"; }};Copy the code
  • Three methods set, get, and remove. ThreadLocal has only three external methods, and the names are easy to understand

    ThreadLocal<String> local = new ThreadLocal<>();
    System.out.println(local.get());  // null
    local.set("111");
    System.out.println(local.get());  / / 111
    local.set("222");
    System.out.println(local.get());  / / 222
    local.remove();
    System.out.println(local.get());  // null
    Copy the code

A simple example of how ThreadLocal can be used is provided in the ThreadLocal source code comment

public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
            new ThreadLocal<Integer>() {
                @Override
                protected Integer initialValue(a) {
                    returnnextId.getAndIncrement(); }};// Returns the current thread's unique ID, assigning it if necessary
    public static int get(a) {
        return threadId.get();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run(a) { System.out.println(ThreadId.get()); } }).start(); }}}Copy the code

ThreadId is a static variable, which belongs to the class and has nothing to do with the new object, but the result is as follows

Every thread that calls ThreadId’s get method gets a different value. ThreadLocal also has a very important use in Android. Looper in Android’s Handler mechanism is stored in ThreadLocal.

ThreadLocal principle

ThreadLocal has only three external methods: set, get, and remove. So we can start with these three methods to study the implementation principle.

ThreadLocal’s set method

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if(map ! =null)
            map.set(this, value);
        else
            createMap(t, value);
    }

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

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
Copy the code
  • Gets the current running thread
  • Gets the internal variable threadLocals of the current thread
  • If threadLocals of the current thread is empty, a new ThreadLocalMap object is created
  • If the current thread’s threadLocals is not empty, the set method of threadLocals is called

There is a threadLocals variable that starts with null. On the first call to ThreadLocal’s set or get methods, a ThreadLocalMap object is created and assigned to threadLocals. ThreadLocalMap we can think of it as a HashMap for now, and we can hold key-values, but we’ll talk more about the implementation of ThreadLocalMap later.

Get method of 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();
    }

    private T setInitialValue(a) {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if(map ! =null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
Copy the code
  • Gets the current running thread
  • Gets the internal variable threadLocals of the current thread
  • If threadLocals is not empty, the map’s getEntry method is called. If entry is not empty, the GET method is hit and the entry value is returned
  • If threadLocals is empty, the setInitialValue method is called
  • The setInitialValue method is very similar to set. SetInitialValue is basically the same as set(initialValue()).

Looking at the get source code, we can see why each thread calling ThreadId’s get method in the previous example got a different ID. Because the map is null when YOU call GET, you end up with the initialValue method.

Remove method of ThreadLocal

     public void remove(a) {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if(m ! =null)
             m.remove(this);
     }
Copy the code

This method is as simple as calling the remove method of ThreadLocalMap.

How ThreadLocalMap works

ThreadLocalMap is a custom hash table that is similar but different from the common HashMap. ThreadLocalMap does not implement a Map interface, and its key can only be a ThreadLocal object. ThreadLocalMap is easy to understand if you are familiar with the source code implementation of HashMap. I have written HashMap source code parsing before, so if you are not familiar with it, take a look.

ThreadLocalMap Internal entity class Entry

        static class Entry extends WeakReference<ThreadLocal<? >>{
            /** The value associated with this ThreadLocal. */Object value; Entry(ThreadLocal<? > k, Object v) {super(k); value = v; }}Copy the code

Entry class is a subclass of WeakReference. WeakReference is one of the four types of reference in Java. The memory pointed to by WeakReference can only survive until the next GC. We’ll leave it at the end to explain why weak references are used as Map keys. Entry also contains a value of type Object, which is the actual value we saved.

Constructor of ThreadLocalMap

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

This constructor takes two arguments, representing the key and value of the first Entry in the Map. As we know from previous analysis, createMap is the constructor that is invoked when ThreadLocal first calls get or set methods. Table is an array of entries. This array is initialized first. The default array size is INITIAL_CAPACITY=16. Here, and then calculate the array subscript firstKey. ThreadLocalHashCode & (INITIAL_CAPACITY – 1) is the top four take threadLocalHashCode binary, algorithms and HashMap how about the agreement would not be in here. The key point here is how does threadLocalHashCode evaluate

    private static final int HASH_INCREMENT = 0x61c88647;
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static int nextHashCode(a) {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    private final int threadLocalHashCode = nextHashCode();

Copy the code

This code is very similar to the example we started with, except that the ids in that example are continuous. In this example, a magic number 0x61C88647 is used to generate hashCode.

Return to the previous constructor. After calculating the index of the array, create an Entry class and place it at table[index]. Finally, set size and threshold. Size is the size of the map, and threshold, like in a HashMap, is the threshold that triggers resize. Look at setThreshold

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

ThreadLocalMap’s load factor is write-dead, fixed at 2/3, meaning that resize is triggered when the number of elements in a ThreadLocalMap exceeds 2/3 of its total capacity

The set method of ThreadLocalMap

    private void set(ThreadLocal
        key, Object value) {
            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)]) { ThreadLocal<? > k = e.get();if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if(! cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
Copy the code
  • ThreadLocalHashCode & (len-1) calculates the array index for the key
  • We iterate backwards from index until we hit null, with the array subscript I
    • If TAB [I].get() == key, the entry already exists and the value of the entry needs to be overwritten
    • If TAB [I].get() == null, the current entry has expired. You need to call the replaceStaleEntry method to replace the expired entry
  • If TAB [index]==null, a new entry is created and stored at the index
  • Sz is the new map size, and if cleanSomeSlots returns false and sz is greater than Threshold, rehash is called

When a ThreadLocalMap encounters a hash conflict, what ThreadLocalMap does is look for the next available location in sequence. This method of resolving hash conflicts is called linear sniffing. It’s really a crude approach, but the advantage is that it doesn’t require extra space. Set does some extra work in addition to assignment, which is mostly to find expired entries and then clean them up. These methods are also very important, so let’s examine them separately

ReplaceStaleEntry method of ThreadLocalMap

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

            int slotToExpunge = staleSlot;
            // Walk forward to find the first entry that expired
            for (inti = prevIndex(staleSlot, len); (e = tab[i]) ! =null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            for (inti = nextIndex(staleSlot, len); (e = tab[i]) ! =null; i = nextIndex(i, len)) { ThreadLocal<? > k = e.get();// A matching key was found
                if (k == key) {
                    e.value = value;
                    // Switch the I and staleSlot positions
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // slotToExpunge points to the position of I because there is already an expired entry at I after switching positions
                    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
            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 name of this method explains what it does: it replaces expired entries. From the call context, we know that in the set method, only the entry is not null and the entry key is null will be executed. If the entry key is null, the key has been recovered by GC and the value saved in the entry is no longer accessible. So this entry is an expired entry. In addition to replacing expired entries, this method also has the function of detecting the presence of other expired entries. If so, the cleanSomeSlot method is triggered. SlotToExpunge holds the start of the clean method

  • Walk forward, stop at NULL, find the last expired entry and record its location with slotToExpunge
  • The array subscript in the traversal is I
    • If TAB [I].get() == key, it indicates that the entry at I matches. In this case, value is assigned to the entry at I and the entry at I and staleSlot are exchanged. Why would I switch places here? I > staleSlot = I > staleSlot = I > staleSlot = I > staleSlot = I > staleSlot = I > staleSlot = I > staleSlot Finally execute cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)
    • If TAB [I].get() == null and the first step forward does not find an expired entry, point slotToExpunge to I
  • If the key is traversed backwards and does not hit, a new entry is created and assigned to TAB [slaleSlot]
  • Finally judge slotToExpunge! = staleSlot, if true, an expired entry was found and cleanSomeSlots(expungeStaleEntry(slotToExpunge), len) should still be executed;

ExpungeStaleEntry method of ThreadLocalMap

        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();if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } 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; }}}return i;
        }
Copy the code

The staleSlot parameter of this method is the slotToExpunge that we have been recording in the replaceStaleEntry method. This method mainly does two things: the first is to empty the table array where the key is null, and the second is to rearrange the entry position. During the clearing process, some positions will be free. Entries whose positions were offset due to hash conflicts may not have conflicts now, so they need to be rearranged. This method also returns a value that returns the position of the next entry == null from staleSlot.

ThreadLocalMap’s 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);
            return removed;
        }
Copy the code

Looking back at the set and replaceStaleEntry methods, you can see that cleanSomeSlots is called in both methods, but the arguments are different.

  • Called in the set method: I represents the position of the newly created entry and n represents the size of ThreadLocalMap
  • Call in replaceStaleEntry: the entry at position I is null, and n represents the size of the table

In either case, the entry at I must not be an expired entry. This method is interesting because instead of doing a full sweep, it does a backward sweep of log2(n) elements, and if none of these elements are problematic, the sweep is not performed. Conversely, if one of the elements is an expired entry, the loop condition is reset and a local cleanup is performed

GetEntry method of ThreadLocalMap

        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);
        }
        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 e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
Copy the code

After analyzing the logic of the set method, get is relatively simple

  • Evaluate index based on threadLocalHashCode
  • If the entry at index matches, the table[index] is returned directly, otherwise the getEntryAfterMiss method is called
  • If k == key, the current entry is returned
  • If k is null, an expired entry is found, and the expungeStaleEntry method is called to perform a local cleanup

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

The process of remove method and GET method is the same. They both search for the corresponding entry, but the result will be returned after GET hits, and the key and value of the entry will be cleared after the remove method hits

Rehash method of ThreadLocalMap

        private void rehash(a) {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

        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); }}private void resize(a) {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            // The capacity is doubled
            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 {
                        // recalculate the position
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while(newTab[h] ! =null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }
Copy the code

When the size of ThreadLocalMap is greater than threshold, rehash is performed. The expungeStaleEntries method does two things. The purpose of the expungeStaleEntries method is to clean up and resize ThreadLocalMap if the size of the cleaned ThreadLocalMap is still greater than 75% of the threshold. The resize method of ThreadLocalMap is similar to the resize method of HashMap, which is doubled. However, ThreadLocalMap does not set an upper limit on the size of the resize. It is also possible that the normal size of ThreadLocalMap cannot be that large. The table must be reordered if the capacity is doubled. Iterate over oldTab, then recalculate the position according to k.tehreadLocalHashCode & (newlen-1), and finally reset size and threshold.

conclusion

ThreadLocal source code analysis is complete, the principle is not complicated but more details, the last picture to help you understand

FAQ

  1. Why use a weak reference as an entry key?

    This problem can be explained by the picture below

    When adding a ThreadLocal value to the current thread, there are two references to the ThreadLocal. The first is to create the returned ThreadLocal strong reference ThreadLocalRef, and the other is to create a weak reference to the key of the ThreadLocalMap. When ThreadLocalRef==null, GC should reclaim the space occupied by ThreadLocal. If ThreadLocalRef==null, GC should reclaim the space occupied by ThreadLocal. The weak reference key still points to ThreadLocal, but this does not affect the GC’s ability to reclaim ThreadLocal. If ThreadLocalRef==null, there will still be a strong reference to a ThreadLocal, and the ThreadLocal will not be reclaimed and leak memory

  2. Memory leak in ThreadLocal?

    The key in a ThreadLocalMap is a weak reference to a ThreadLocal, so the ThreadLocal will be collected when it is not used, but the Entry value is a strong reference. If no cleanup of ThreadLocalMap is triggered, the value will persist until the thread terminates. This problem is particularly pronounced on Android, where the main thread continues until the application exits, resulting in a long memory leak. If you’re interested, you can test it with the following classes

    public class TestTLObject {
        private static final String TAG = TestTLObject.class.getSimpleName();
        public int[] arr = new int[1024 * 1024 * 10];
    
        @Override
        protected void finalize(a) throws Throwable {
            super.finalize();
            Log.e(TAG, "Garbage Collection Execution");
        }
    }
    
    ThreadLocal<TestTLObject> threadLocal = new ThreadLocal<>();
    threadLocal.set(new TestTLObject());
    // threadLocal.remove();
    threadLocal = null;
    Copy the code