JAVA concurrency's ThreadLocal source code details, 80% of people will not

Keywords: Java Programming

summary

1. In concurrent programming, in order to control the correctness of data, we often need to use locks to ensure the execution isolation of code blocks. But in many cases, the cost of locking is too large. In some cases, our local variables are thread private, and each thread will have its own variable / quantity. At this time, we can not lock this part of data. So ThredLocal came into being.

2, As the name implies, ThredLocal is a local variable held by a thread. Variables stored in ThredLocal will not be synchronized to other threads and the main thread. All threads are invisible to other thread variables. So let's see how it works.
3. Note: light theory is not enough. In this free gift of 5 JAVA architecture project practical course and large factory interview question bank, interested can get into skirt 783802103, do not enter without foundation!

Implementation principle

ThredLocal internally implements a static class ThreadLocalMap to store variables, and uses these two member variables within the Thread class

    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

To call ThreadLocalMap to store the internal variables of the current thread.

Implementation of ThreadLocalMap

ThreadLocalMap is a map of key value structure, but it does not directly use HashMap, but implements one by itself.

Entry

Entry is the map node defined in ThreadLocalMap. It takes ThreadLocal weak reference as key and Object as value in K-V form. Weak references are used to release memory in time to avoid memory leaks.

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

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

The difference between HashMap and HashMap is that they have different ways to solve the hash conflict. HashMap adopts the chain address method. In case of conflict, put the conflicting data into the same chain list, and then convert the chain list into a red black tree when the chain list reaches a certain level. The implementation of ThreadLocalMap adopts the open addressing method. It does not use the linked list structure internally, so there is no next or prev pointer inside the Entry. See the following source code for how to implement the open addressing method of ThreadLocalMap.

Member variable

    // map default initialization size
    private static final int INITIAL_CAPACITY = 16;

    // An array for storing map node data
    private Entry[] table;

    // map size
    private int size = 0;

    // Critical value. When it reaches this value, it needs to be expanded
    private int threshold;

    // Capacity expansion when the critical value reaches 2 / 3
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

The array size here is always a power of 2, for the same reason as HashMap, to reduce collisions when calculating hash offsets.

Constructor

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // Initialize table
        table = new Entry[INITIAL_CAPACITY];
        // Calculate the hash value of the first value
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // Create a new node
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

set method

    private void set(ThreadLocal<?> key, Object value) {

        // Get the hash offset of ThreadLocal
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);

        // Traverses the array until the node is empty
        for (Entry e = tab[i];
                e != null;
                e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();

            // If the node key s are equal, we find the node we want,
            // Assign values to nodes
            if (k == key) {
                e.value = value;
                return;
            }

            // If the key of the node is empty, it means that the weak reference has recycled the key, so a wave of cleaning is needed
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        // If no corresponding node is found, the key does not exist. Create a new node
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // Clean up. If the cleaning result fails to clean up any old nodes,
        // And if the array size exceeds the critical value, rehash is performed
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

Seeing this code, the implementation principle of open addressing is very clear. First, calculate the hash value of the node, find the corresponding location, and check whether the location is empty. If it is empty, insert it. If it is not empty, extend it to the next node until the empty location is found. Then our query logic is ready to come out: calculate the hash value of the node, find the corresponding location, check whether the node is the node we want to find, and if not, continue to search in the next order.

get method

    private Entry getEntry(ThreadLocal<?> key) {
        // Calculate hash value
        int i = key.threadLocalHashCode & (table.length - 1);
        // Get the array node corresponding to the hash value
        Entry e = table[i];
        if (e != null && e.get() == key)
            // If the node is not empty and the key is the same, it means that the node we are looking for is returned directly
            return e;
        else
            // Otherwise, keep looking back
            return getEntryAfterMiss(key, i, e);
    }
    
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;

        // If the node is not empty, keep searching
        while (e != null) {
            ThreadLocal<?> k = e.get();
            // If the key is the same, it means to find it and return the node
            if (k == key)
                return e;
            // Clear once when key is empty
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

replaceStaleEntry

    // The function of this method is to clean up during set operation
    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;
        
        // Slotttoexpunge is the node location where cleanup will start later
        int slotToExpunge = staleSlot;
        // Go ahead and find the first empty node and record the location
        for (int i = prevIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;

        // Start from staleSlot and traverse backward until the node is empty
        for (int i = nextIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();

            if (k == key) {
                // If the key of the node is the same, replace the value
                e.value = value;

                // Swap the current node with the node on the staleSlot (put the value in the back to the front, and wait for the previous value to be recycled)
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                // If the slotToExpunge and the staleSlot are equal, there are no nodes to clean up in the front
                // Clean up from the current node
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                // Perform node cleanup
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

            // If the key is empty and the slotToExpunge and staleSlot are equal
            // Assign slotttoexpunge to the current node
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // If you can't find a node with the same key,
        // Clear the value of the current node and generate a new node
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // If the slotToExpunge and staleSlot are not equal, you need to clean up (because empty nodes are found in front)
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

expungeStaleEntry

    // Clean up nodes
    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // Release current node
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        Entry e;
        int i;
        // Loop to find the first empty node
        for (i = nextIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            // Release node when key is empty
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                // If the key is not empty, find the location where the corresponding node should be
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    // If it is different from the current node location,
                    // Then clean up the node and loop to find the non empty node behind and move to the front
                    tab[i] = null;

                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

cleanSomeSlots

    // This method is used to clean up empty nodes
    private boolean cleanSomeSlots(int i, int n) {
        // Mark whether any nodes are cleared
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            // If any node is empty and key is empty
            // This node needs to be cleared
            if (e != null && e.get() == null) {
                // Reset the value of n and mark removed as true
                n = len;
                removed = true;
                // Clean up the node
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }

rehash

    // When the elements of the array reach the critical value, expand the capacity
    private void rehash() {
        // Clean up all nodes first
        expungeStaleEntries();

        // Then judge whether the critical value is expanded
        // Because of the first cleaning, the number here may be smaller than the previous critical value judgment
        // So the critical value here is determined as threshold - threshold / 4
        // That is, 1 / 2 of the size
        if (size >= threshold - threshold / 4)
            resize();
    }

    private void resize() {
        // Get old array, open up new array
        // The new array is twice the size of the old one
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];
        int count = 0;

        // Traverse old array
        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                // If the node is not empty, judge whether the key is empty
                // If the key is empty, leave the node empty to help gc
                // If the key is not empty, put the nodes of the old array into the new array
                // The insertion method is consistent with the set implementation, only because it is the new array just created
                // There will be no data to be cleaned up, so no additional cleaning is required
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                } else {
                    int h = k.threadLocalHashCode & (newLen - 1);
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    newTab[h] = e;
                    count++;
                }
            }
        }

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

expungeStaleEntries

    // Clean up all nodes
    private void expungeStaleEntries() {
        Entry[] tab = table;
        int len = tab.length;
        // Circulation cleaning
        for (int j = 0; j < len; j++) {
            Entry e = tab[j];
            if (e != null && e.get() == null)
                expungeStaleEntry(j);
        }
    }

About Map cleanup

The implementation of ThreadLocalMap adopts the open addressing method, and its implementation itself should be relatively simple. However, in order to facilitate GC, the internal node uses weak reference as key. Once the strong reference of the node in the array is set to null, the key of the node will be automatically recycled by GC. This makes the implementation of ThreadLocalMap extremely complex. In order to prevent memory leakage, extra cleaning has to be done during get and set methods.

Q why do I need to clean up?

If A does not clean up, the key will be recycled, but the value will still exist, and it is difficult to recycle, resulting in memory leakage.

Q why is node movement involved in cleaning?

A because in the open addressing method, the nodes with the same hash value may be arranged together continuously. When one or more nodes are recycled, there will be null nodes among the nodes with the same hash value, and when we get nodes, we will stop searching when encountering empty nodes. Therefore, if we don't do some cleaning and moving, some nodes will never be queried.

Implementation of ThreadLocal

The implementation of hashcode

After talking about the implementation principle of ThreadLocalMap, we can deeply realize how important the hashcode of ThreadLocal is. If the hash value cannot be generated in a reasonable way, resulting in uneven data distribution, the efficiency of ThreadLocalMap will be very low.

Implementation of hashcode:

    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

The hashcode implementation code of ThreadLocal is very short: the hash value of each new ThreadLocal is increased by 0x61c88647 on the basis of nextHashCode. The implementation is simple, but confusing. What is this inexplicable magic number 0x61c88647?

0x61c88647 is a gold proportion number constructed by Fibonacci. Through experimental tests, the hashcode generated by this number can largely ensure that the hash value can be evenly distributed in the array.

get

    public T get() {
        // Get current thread
        Thread t = Thread.currentThread();
        // Get the variable map of the current thread
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // Find value return
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // Return default if not found
        return setInitialValue();
    }

set

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // Add data if map is not empty
        if (map != null)
            map.set(this, value);
        else
            // Otherwise, create a new map and put the first and data
            createMap(t, value);
    }

summary

1. Thredlocal may be a commonly used class for many people, but not everyone will pay attention to its internal implementation, but its source code is worth reading. First, its implementation code is relatively short compared with other commonly used classes, only a few hundred lines; second, its implementation is classic, classic open addressing method, classic weak reference is convenient for GC, It can be said to be a good learning material. Although I have explained the source code of the whole thredlocal completely here, the most worthy thing is its design concept and design ideas, which will play an important role in writing excellent code.
2. Note: light theory is not enough. In this free gift of 5 JAVA architecture project practical course and large factory interview question bank, interested can get into skirt 783802103, do not enter without foundation!

The text and pictures of this article come from the Internet and my own ideas. They are only for learning and communication. They have no commercial use. The copyright belongs to the original author. If you have any questions, please contact us in time

last

 

Posted by no_maam on Thu, 04 Jun 2020 07:09:00 -0700