(22) ThreadLocal

Foreword: Today, I reviewed the knowledge of handler (1) on a whim , and found a knowledge point ThreadLocal that was forgotten at the time. I really didn’t understand it at the time. Now to summarize.

ThreadLocal: MessageQueue objects and Looper objects have only one object in each thread. How can it be guaranteed that it has only one object and is saved through ThreadLocal. Thread Local is a data storage class inside a thread, through which data can be stored in a specified thread. After the data is stored, the stored data can only be obtained in the specified thread, and the data cannot be obtained for other threads. (This was not very clear at the time, but now I understand, because once a thread calls Looper.prepare, it will save the Looper in the local variable ThreadLocalMap held by the thread alone, using the thread as the key value and Looper as the values ​​value , of course it cannot be shared with other threads, and due to the particularity of prepare, the looper will only be saved once, which ensures that there is only one looper, and it is the one after prepare; Looper is unique, and messageQueue is naturally unique as its member variable)


Refer to know the answer: winwill2012's answer


1 Introduction

The good part of this answer is that it combines source code comments, example explanations and source code explanations, which are step-by-step and convincing. I also prefer to start learning from source code comments after having a simple understanding of the basics.

The source code comments are as follows. In short, ThreadLocal provides thread-level local variables. The difference between these variables and ordinary local variables is that each thread has its own variables, which can be initialized separately by get() and set() ) to read and write. ThreadLocal is generally a private static variable associated with Thread. As long as each thread is alive, there is an implicit reference to ThreadLocal. If the thread hangs, all ThreadLocals there will be garbage collected if there is no other place to reference it.

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code 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).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * 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() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * <p>Each thread holds an implicit reference to its copy of a thread-local
 * variable as long as the thread is alive and the {@code ThreadLocal}
 * instance is accessible; after a thread goes away, all of its copies of
 * thread-local instances are subject to garbage collection (unless other
 * references to these copies exist).
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */


2. Principle

The set() and get() methods were mentioned in the source code before. After reading the source code of set(), I have a deep understanding of the implementation principle of ThreadLocal.

2.1 The principle from set()

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

It's actually very well written here. Simply put, it is divided into 3 steps:

1. Get the identity of the current thread

2. Get the ThreadLocalMap variable held by the current thread itself

3. If the variable is not empty, set an entry directly, use threadLocal as the key, and use the real value as the value.


The second step is explained here. Thread has a member variable called threadLocals, which is a map internal class defined inside ThreadLocal.

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal's ThreadLocalMap static inner class.

This inner class can be regarded as a map that simply uses the open address method to resolve hash conflicts. The special thing is that ThreadLocal is used as an instance. Since each thread holds its own unique ThreadLocalMap variable, even if the ThreadLocal is the same, because it is in a different map, the value accessed by the ThreadLocalMap will not be affected by other threads, which achieves the purpose of thread local variables.


2.2 Looking at memory leaks from ThreadLocalMap

Quoting the original text:

/* Then there are rumors on the Internet that ThreadLocal will cause memory leaks. Their reasons are as follows:

As shown in the figure above, ThreadLocalMap uses the weak reference of ThreadLocal as the key. If a ThreadLocal has no external strong reference to reference it, then the ThreadLocal will be recycled when the system is gc. In this way, an Entry whose key is null will appear in ThreadLocalMap. There is no way to access the value of the Entry whose key is null. If the current thread is delayed, there will always be a strong reference chain for the value of the Entry whose key is null:

ThreadLocal Ref -> Thread -> ThreaLocalMap -> Entry -> value

It can never be recycled, causing a memory leak.

Let's see if this happens. In fact, this situation has been considered in the design of JDK's ThreadLocalMap, and some protective measures have been added. */

First of all, the static inner class Entry held by ThreadLocalMap is inherited from WeakReference. It is mentioned in the comments that if the key becomes null, it means that this entry needs to be removed from the table, and these entries are treated as outdated.

/**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

2.2.1 Initialization

It is initialized to an array of 16, which is similar to a hashmap, and then the key of the first element is the first ThreadLocal to be called, through which the position in the array is calculated in the form of a hash table, and the corresponding value is generally The value generated by calling setInitialValue.

/**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        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);
        }


2.2.2 set()

The set() method can be understood as adding elements to the table in the form of key-value pairs. There is no chain address method to solve hash conflicts like hashmap, but the open addressing method is used, that is, the position is occupied and the next position is extended until a position is empty.

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

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

When the key is null, the replaceStaleEntry method will be called to clear the outdated entry. Simply understand the source code, it will use the staleSlot as the benchmark to find the key value is also null, and then use the key value to be null The index where the element is located is the starting parameter, and the emptying operation of the entry whose key is null is started. Why not use staleSlot as the clearing parameter, because the location of the staleSlot is either replaced or set. In the process of clearing, one is also cleared because of the storage location determined by the open address method. If the latter is delayed, it is moved to the front, that is, the so-called rehash, to ensure the order of hash calculation.

/**
         * Replace a stale entry encountered during a set operation
         * with an entry for the specified key.  The value passed in
         * the value parameter is stored in the entry, whether or not
         * an entry already exists for the specified key.
         *
         * As a side effect, this method expunges all stale entries in the
         * "run" containing the stale entry.  (A run is a sequence of entries
         * between two null slots.)
         *
         * @param  key the key
         * @param  value the value to be associated with key
         * @param  staleSlot index of the first stale entry encountered while
         *         searching for key.
         */
        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).
            //Go forward to find an element with an empty key value and record its position.
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (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
            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) {
                    e.value = value;

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


2.2.3 getEntry()

getEntry is somewhat similar to set in that it will perform some special operations when it encounters that e is empty or the keys of entry are not equal.

/**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        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);
        }

When k == null, the expungeStaleEntry method is called to clean up. The cleanup operation seems to be a one-time operation, unlike calling cleanSomeSlots when set to perform additional log2(n) checks.

/**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        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;
        }
/**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        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;
        }

2.2.4 Memory leak and remove

Quoting the original text:

/* Entry with null key encountered in this process will be erased, so the value in the Entry will not have a strong reference chain and will be recycled naturally. If you study the code carefully, you can find that the set operation also has a similar idea. All the entries whose key is null are deleted to prevent memory leaks. But this is not enough. The above design idea depends on a precondition: to call the genEntry function or set function of ThreadLocalMap. This is of course impossible to hold in any situation, so in many cases, the user needs to manually call the remove function of ThreadLocal, and manually delete the ThreadLocal that is no longer needed to prevent memory leaks. Therefore, JDK recommends that ThreadLocal variables be defined as private static. In this case, the life cycle of ThreadLocal will be longer. Since there is always a strong reference to ThreadLocal, ThreadLocal will not be recycled, which can ensure that ThreadLocal can be used at any time. Weak reference accesses the value of the Entry, and then removes it to prevent memory leaks.
Author: winwill2012
Link: https://www.zhihu.com/question/23089780/answer/62097840
Source: Zhihu The
copyright belongs to the author. For commercial reprints, please contact the author for authorization, and for non-commercial reprints, please indicate the source. */

Look at the so-called remove method, which will actively call the expungeStaleEntry method to clean up expired entries and prevent memory leaks.

/**
         * Remove the entry for key.
         */
        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;
                }
            }
        }

Looking at the Looper source code, it does not show that the move method is called to clear the ThreadLocal.


3. Summary

ThreadLocal can be regarded as a local variable of Thread. More deeply, it can be said to be the value of thread as the key of its member variable map. Each thread call will initialize it by itself (calling initialValue()), which isolates other threads from meddling. possible.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325852184&siteId=291194637