Confused by the interviewer again? It's better to take ThreadLocal apart and crush it to see if you can understand more!

Posted by vixtay on Fri, 18 Feb 2022 18:10:03 +0100

Xiaobian's words

At the end of the article, the author sorted out a lot of materials for you! Including Java core knowledge point + full set of architects learning materials and videos + Kwai Tai interview Treasure + interview resume template + Ali group NetEase, Tencent Iqiyi millet quick Iqiyi beep miles interview question +Spring source code collection, +Java architecture, real e-book and so on!


The information is shared free of charge, Just click here and download it directly

preface

1. Why use ThreadLocal?

The so-called concurrency means that limited resources need to deal with access far beyond resources. The way to solve the problem is to increase resources to deal with access; Or increase the utilization of resources. Therefore, I believe that more or less developers these days will have a few "thread two or three moves" and "five or six locks". What that brings is the concurrency security problem under multithreaded access. The access domain of shared variables spans the original single thread and enters the eyes of thousands of threads. Anyone can use it and anyone can change it. Isn't that a fight? Therefore, the best way to prevent concurrency problems is to avoid multi-threaded access (this technological level has regressed by 20 years ~). ThreadLocal, as its name suggests, limits a variable to "thread closure": an object is held, accessed, and modified by only one thread.

2. What is ThreadLocal?

If ThreadLocal does thread closure, it is difficult to support. It is bound to join hands with thread to bring good news to the majority of javaers. ThreadLocal itself is not a storer, it is just a porter of thread. Unique variables must exist in thread. Generally, multiple ThreadLocal are defined in a project, and the corresponding thread must also store so many unique variables. Since the access interference between threads is solved, the access interference of a thread is not a problem. Thread maintains a ThreadLocalMap and stores unique variables in the form of "key value"; Take the ThreadLocal instance as the key to accurately obtain.

3. What issues should ThreadLocal consider?

If the thread dies, ThreadLocalMap, ThreadLocal and unique variables will be destroyed.

But now avoid the repeated creation and destruction of threads. Threads are put back into the thread pool after use. If the element of ThreadLocalMap is not manually removed, even if the current thread exits, ThreadLocal is no longer held by the thread method stack and cannot be recycled, resulting in memory leakage. So ThreadLocalMap The key of entry (that is, ThreadLocal) is actually a weak reference. When there is no other strong reference, as long as GC occurs, it will be recycled, which means that the key is null at this time.

This raises another problem. The key is recycled, but the entry and value are still strongly referenced. What should we do? ThreadLocalMap has considered this situation. When calling the set(), get(), and remove() methods again, it will clean up the records with null key. So there is no problem with other people's design. If there is a memory leak, it is wrong to use it. It is recommended to call the remove() method manually after using the ThreadLocal method.

4. What other issues should ThreadLocal consider?

With the complexity of business scenarios, thread closure of variables not only solves the problem of access, but also brings difficulty to thread transmission. The cooperation between threads brings the need for safe transmission of variables between two threads. There are three steps to deal with this transfer manually:

  • Thread 1 takes out variables;
  • Thread 1 safely passes variables and ThreadLocal (in fact, it generally chooses to share) to thread 2. Be careful to escape.
  • Thread 2 is placed in the ThreadLocal of the current thread. This step is general. As long as ThreadLocal is used and thread delivery is required, these three steps are indispensable. JDK provides us with the solution of "thread 2 is that when thread 1 is created, unique variables are passed to thread 2": InheritableThreadLocal. There is also a ThreadLocalMap in thread that serves it specially.

Then we understand that in the world of online process pooling, there will not be frequent creation scenarios, but more collaboration with existing threads. In fact, companies will also develop their own class libraries for ThreadLocal of related businesses to achieve delivery. The class library for thread delivery in common scenarios on the market is TransmittableThreadLocal.

Source code analysis

Thread

public Class Thread implements Runnable {

    //ThreadLocal value associated with this thread. Maintained by ThreadLocal class
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // The InheritableThreadLocal value associated with this thread. Maintained by the InheritableThreadLocal class
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocalMap is an internal class of ThreadLocal and a customized Map implementation. The initial values are null. It will be initialized only when the get or set corresponding to ThreadLocal is called for the first time.

ThreadLcoal

ThreadLcoal has only one default parameterless constructor. The actual initialization logic is when get or set is called for the first time.

get()

Because it is similar to lazy loading, get involves the creation of ThreadLocalMap and the setting of initial value.

public T get() {
    Thread t = Thread.currentThread();
    // To get the map of a thread, why extract the method? This is to extend the InheritableThreadLocal mentioned earlier
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    // Already set
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
    @SuppressWarnings("unchecked")
    // There is no Entry here: after remove
    T result = (T)e.value;
    return result;
    }
    }
    // First get without set (map = = null)
    // Or set, but remove (map! = null & & E = = null)
    return setInitialValue();
}

private T setInitialValue() {
    // Gets the specified initial value, which is null by default
    // ThreadLocal with specified initialization value can be created through withinitial (supplier <? Extensions s > Supplier) factory method
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        // ThreadLocalMap is not initialized
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        // Logic for handling a special subclass
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}   

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

Specifies the factory construction method for the initial value

// If you get for the first time under the following circumstances, judge that the entry of the map is null
// 1. Never set;
// 2. After remove
protected T initialValue() {
    return null;
}

The default initial value is null. You can obtain a ThreadLocal specifying the initialization logic through the following factory methods.

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

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

set() & remove()

set() is a bit like setInitialValue(), except that one is the initial value and the other is the specified value.

Both methods are simple in themselves and mainly depend on the operation of ThreadLocalMap.

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

ThreadLocalMap

  • This class is the internal class of ThreadLocal and is private to the package.
  • The hashcode of the key is a user-defined growth value.
  • Yeah, yeah.

Entry

You can see that the key is ThreadLocal, which is definitely not empty, but it is also weakly referenced.

In other words, when the key is null, it indicates that ThreadLocal has been recycled and the corresponding Entry should be cleared.

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

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

Preset value

  • The initial capacity is 16, and the expansion capacity is doubled. So the capacity must be the n-th power of 2.
  • The load factor is 2 / 3.
  • During initialization, the value should be set for the first time or from ThreadLocalMap. So it can be regarded as hungry man loading.
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

Constructor

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

ThreadLocal {
    /**
     * Artificially set hash code distribution For ThreadLocal constructed continuously in the same thread, it can effectively avoid conflict
     * Because it is a predictable scenario, it is only used in ThreadLocalMap
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =  new AtomicInteger();

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

Each ThreadLocal object has a hash value threadLocalHashCode. Each time a ThreadLocal object is initialized, the hash value increases by a fixed size 0x61c88647. This thing is quite exquisite. If you are interested, you can study it yourself.

set()

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;
         // Open addressing method: index position + 1
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            // If the key is empty, the corresponding ThreadLocal has been recycled
            // You can reuse the current location
            // There are two situations: 1 \ Entry exists behind this obsolete location So it needs to be replaced to this position
            // 2. It doesn't exist. Put it directly in this position
            replaceStaleEntry(key, value, i);
            // Because it is a replacement, the size is either unchanged or reduced.
            return;
        }
    }

    // No existing or replaceable obsolescence was found Directly create
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // If the obsolete entry is not cleared and the threshold is exceeded Try to shrink first, or expand if not
        rehash();
}

Class defines two methods for open addressing lookup: increment is 1.

private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

replaceStaleEntry()

The replaceentry () is complex. First, the obsolete entry needs to be cleared. Second, the open addressing method needs to ensure the continuity of the elements behind the calculated index value.

Therefore, replacestate entry () will check all obsolete entries between the nearest two neutral positions before and after the current replaceable position.

Secondly, if the key already exists behind the outdated position, the space will be left after the original position is replaced, and the entry behind needs to be moved forward one position (before the space).

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

    int slotToExpunge = staleSlot;
    // 1. Look forward to the minimum out of date after the first neutral
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Move forward to find the key before the first gap or the maximum out of date
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // Find the corresponding entry
        if (k == key) {
            e.value = value;
            // 2. Replace the key with the old position
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            if (slotToExpunge == staleSlot)
                // 3. If there is no obsolescence in the front, the first obsolescence in this interval is the original staleSlot, now i
                slotToExpunge = i;
            // 4. Clean up obsolete and move the entry
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 5. If there is no empty slot in front and there is a new obsolete, re mark the first obsolete (because the staleSlot will be replaced with one that is not outdated, it will not be the first outdated point at that time)
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    // 6. Direct replacement
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // slotToExpunge == staleSlot, which means that only the current interval is obsolete and has been replaced, so it does not need to be cleared
    if (slotToExpunge != staleSlot)
        // The key is not present, and there are other obsolescence before or after it
        // 7. Clean up obsolete and move the entry
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

5. 7. As it is out of date, it will be explained in detail later.

The interval is the range composed of the first vacancy before and after the current staleSlot, that is, between the two blank grids in the figure below.

According to the different conditions of the interval, we have made illustrations.

key exists:

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-rqvpvi3c-1619590123594) (/ / upload images. Jianshu. IO / upload_images / 20063139-bff7b7aa266cc82a. PNG? Imagemogr2 / Auto orient / strip|imageview2 / 2 / w / 1200 / format / webp)]

key does not exist:

[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-rbgpaf4n-1619590123597) (/ / upload images. Jianshu. IO / upload_images / 20063139-4cd0646a3c1f06b9? Imagemogr2 / Auto original / strip|imageview2 / 2 / w / 1200 / format / webp)]

rehash()

When set() is finished and the quantity reaches the threshold, try to delete some outdated ones first. If there is nothing to delete, or the deletion fails to meet the standard, the capacity will be expanded.

Note that this standard is not the previous threshold, but 3/4 threshold to avoid hysteresis.

private void rehash() {
    // Scan and clean up the entire array
    // Instead of the replacement step, only scan the interval
    expungeStaleEntries();

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

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
            // Double expansion
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                // If it is found to be outdated, it will be discarded
                e.value = null; // Help the GC
            } else {
                // Re hash
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

private void expungeStaleEntries() {
    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);
    }
}

expungeStaleEntry()

From the above analysis, we can see that this method is applied to replacestateentries and expingestaleentries.

Replacestateentry is used to process intervals, and expungeStaleEntries is used to process full arrays. Therefore, expungeStaleEntry (int) is a subset of the above processing. In this way, it is to clean up the obsolete entry between the specified location and the next empty bit, including the specified location: [index, indexOf(first null)).

  • index must be the location of an obsolete element.

  • Since the outdated will be cleared away, a space will be left in the middle. The open addressing method requires continuity, so the index is recalculated to place.

  • Note: the reserved key is to recalculate the index, not simply move forward one bit.

  • This is because the clearing interval is obsolete, which is before a key and the calculated starting index.

  • The key is just on this index. Simply move it forward, and you may not find it next time.

  • Because it requires continuous traversal from beginning to end, once there is a vacancy in the middle, it can't be found.

private int expungeStaleEntry(int staleSlot) 
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    // Make it clear that the current location must be outdated. Clean it directly first
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    // Start traversing until the first empty bit is encountered
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // Clean up obsolescence
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // Because there is a gap in the front, the subsequent elements have to recalculate the index in order to fill the gap
            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;
            }
        }
    }
    // Returns the first empty bit encountered
    return i;
}

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];
        // There are no obsolete slots, and a total of log2(n) slots are scanned;
        if (e != null && e.get() == null) {
            // If the above period is out of date, the interval will be traversed:
            // Then re scan log2(length) based on the end point of the interval;
            // If it is scanned, repeat the above;
            // If it keeps repeating, the whole array is finally scanned.
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

cleanSomeSlots is generally scanned or cleared after adding an element or deleting another old element (not remove, but after deleting another obsolete element during set).

The starting position is an element, not an outdated index, but the end point (empty bit) after scanning an interval or the position of new elements.

At the end point, because the logarithmic scan is used, it is a balance of two extreme cases:

  • There are no obsolete slots, and a total of log2(n) slots are scanned;

  • If the above period is out of date, the interval will be traversed;

  • Then re scan log2(length) based on the end point of the interval;

  • If it is scanned, repeat the above;

  • If it keeps repeating, the whole array is finally scanned.

get & remove

get:

  • If found directly, return;
  • If not, traverse the search in the increment of open addressing method. In this process, you also need to clear the obsolete (expingestaleentry (int)) in the interval.

remove:

Find the specified key and clear it. Similarly, clear the key in the interval.

Memory leak

After the above analysis, the key, that is, ThreadLocal, is a WeakReference in the Entry.

If a GC occurs when ThreadLocal has no external strong reference, the weak reference of ThreadLocalMap will not affect the recycling.

That is equivalent to key = null in the Entry, but both Entry and Value are strong references and cannot be destroyed with the key.

Think about the role of ThreadLocal. When ThreadLocal is destroyed, the storage of key value is meaningless.

If you wait for a part-time task to eliminate obsolescence, there is also a time difference. When value is a large object, it is also more troublesome.

Therefore, it is suggested that:

When exiting after use, it is best to use ThreadLocal The remove () method actively removes the variable.

InheritableThreadLocal

When thread 2 is created from thread 1, you can specify whether to inherit ThreadLocal from thread 1. Of course, the premise is that thread 1 uses an inheritable ThreadLocal.

private Thread(ThreadGroup g, Runnable target, String name,
               long stackSize, AccessControlContext acc,
               boolean inheritThreadLocals) {
    ...... ellipsis
    Thread parent = currentThread();
    // parent.inheritableThreadLocals is not empty. InheritableThreadLocal must be used for the current thread 
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ...... ellipsis
}

ThreadLocal :

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
// For createInheritedMap only
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (Entry e : parentTable) {
        if (e != null) {
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                // The operator class values calculated from the parent class are the same by default
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    // The hash conflict handling method is the open addressing method
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

InheritableThreadLocal:

You can see that using InheritableThreadLocal, the variable of the Thread of the operation is different from ThreadLocal.

It just corresponds to the Thread created above and inherits the inheritableThreadLocals of the parent Thread.

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        // Calculate the subclass value from the parent value, which can be overridden
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
        // The obtained map is different
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        // Different use of map
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

Topics: Java Android Interview Multithreading Programmer