Mysterious veil of Java local thread variable ThreadLocal

There should be no one who hasn't used this thing!

ThreadLocal

Role: thread isolation

1. Methods provided

  • get()
  • set()
  • remove()

2. Used in Java

static ThreadLocal<ReqData> threadLocal = new ThreadLocal<ReqData>(){
    @Override
    protected ReqData initialValue() {
        //Override the initialValue method to return the variable object stored by the current thread
        return new ReqData();
    }
};
//java8 syntax
static ThreadLocal<ReqData> threadLocal = ThreadLocal.withInitial(() -> new ReqData());

public static void main(String[] args) {
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            threadLocal.get().setName(Thread.currentThread().getName());
            threadLocal.get().setReqId(UUID.randomUUID().toString());
            System.out.println(threadLocal.get().toString());
        }).start();
    }
}

@Data
static class ReqData {
    String name;
    String reqId;
}
//output
ThreadLocalDemo.ReqData(name=Thread-2, reqId=df3e550f-48dd-49fa-99b5-8889058cfad1)
ThreadLocalDemo.ReqData(name=Thread-0, reqId=b81a34e7-3507-445c-81ca-bea07b6e4626)
ThreadLocalDemo.ReqData(name=Thread-1, reqId=6aaa376c-1379-4cbf-a9ad-d509ff4aec9b)

3. Source code analysis

What is linear detection before looking at the source code?

Linear detection is a strategy adopted by computer programs to solve hash table conflicts
In order to search for the given key x, the adjacent cells h(x) + 1,h(x) + 2,... Starting from the cell corresponding to h(x) in the hash table will be checked until the cell with empty content or the cell storing the given key x is found. Where h is the hash function. If a cell storing a given key is found, the search returns the value corresponding to the key stored in the cell.

Simply put, when inserting array subscript X, when the key value of subscript X is consistent with the inserted key value, it may be directly overwritten. When it is inconsistent, it will be overwritten from the adjacent subscripts X+1, X+2 Will be detected and will not be stored in the current subscript space until the content is empty or the key is consistent

  • The underlying data structure, each thread has an independent Entry space
  • key: hashcode of the reference value of Threadlocal. In case of hash collision, it is necessary to linearly calculate the new location and remove the dirty data
  • Vaules: objects
  • Capacity expansion: generate a new array and copy the original array

3.1 initialvalue 𞓜 withinitial initialization

static ThreadLocal<ReqData> threadLocal = new ThreadLocal<ReqData>(){
    @Override
    protected ReqData initialValue() { //Overridden the initialValue method to return the thread variable object
        return new ReqData();
    }
};

3.2 initialization and structure

ThreadLocal.ThreadLocalMap

//The WeakReference weak reference is used to free the thread
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    //Our custom storage objects
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
//Array of initial size16 of core structure
private Entry[] table;

3.3 set() method

public void set(T value) {
    Thread t = Thread.currentThread();
    //Each thread has a ThreadLocal ThreadLocalMap threadLocals;
	//Gets the ThreadLocalMap of the current thread
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
    	//Lazy loading mode is described later
    	//t.threadLocals = new ThreadLocalMap(this, firstValue);
        createMap(t, value);
}

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    //Calculate the array subscript through hash
    int i = key.threadLocalHashCode & (len-1);
    //The main method used here is linear exploration. Only when the Entry at the corresponding position is not empty can the for loop be entered
    //1. If the key in the entry is the same as the currently passed in key, it will be overwritten directly
    //2. If the key in the Entry is empty (ThreadLocal weak reference), it is necessary to clean up the dirty Entry and assign values at the same time
    //If there is no return, continue to find the next subscript 
    //nextIndex -> ((i + 1 < len) ? i + 1 : 0); // Add 1 at a time to the down mark position
    //prevIndex -> ((i - 1 >= 0) ? i - 1 : len - 1); // Subtract 1 at each downward position
    for (Entry e = tab[i];
         e != null;
         //Linear exploration i will be assigned
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
		//If the current position is not empty and the key value is equal to the current thread key, it will be overwritten directly
        if (k == key) {
            e.value = value;
            return;
        }
		//If it is empty, clean up and assign values from the current subscript
        //Replacestateentry - > first clean up the dirty data from the i array position, then clean up the dirty data later, and then store the current key value into the
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	//If the array subscript position is not occupied, it will be assigned directly
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //First go through the dirty data cleaning of the current subscript to the array size+1. Whether there are cleaning elements will be returned
    //If there is no element that can be cleared, it indicates that there are values from the current node to the array length. Consider whether to expand the capacity
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

3.4 get() method

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //Get Entry from Entry array
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //map is empty and has not been initialized
    return setInitialValue();
}
  • getEntry()
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //return if you can find it directly
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

//If the Entry in the current position is not equal to the current key, continue to look down. If you don't understand, see set linear addition
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)
        	//Clean up dirty data from the current subscript position
            expungeStaleEntry(i);
        else
        	//Otherwise, the subscript is incremented
            i = nextIndex(i, len);
        //After the above cleaning, if it is empty, it will jump out of the loop    
        e = tab[i];
    }
    return null;
}
  • setInitialValue initialize ThreadLocal
private T setInitialValue() {
    //Calling our own implementation code returns an initialization object
    T value = initialValue();
    Thread t = Thread.currentThread();
    //Because the current is not an atomic operation, get it again
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //If another thread has already created it, set it directly
        map.set(this, value);
    else
        //Otherwise, the ThreadLocalMap incoming thread and the object for storage are created
        createMap(t, value);
    return value;
}
  • createMap initializes the ThreadLocalMap method
//Both get and set are created by lazy loading
void createMap(Thread t, T firstValue) {
    //Assign this - > current thread to the thread variable ThreadLocalMap
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//The Entry array of ThreadLocalMap in the initialization thread stores the stored objects
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //Initialize Entry array
    table = new Entry[INITIAL_CAPACITY];
    //Calculate the array index according to the reference of the current thread - > hashcode
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //Save stored objects
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //Set expansion factor len * 2 / 3, size 16 - > up to 10
    //The expansion here is to generate a new entry array, and then recalculate and migrate the old values
    setThreshold(INITIAL_CAPACITY);
}

3.5 remove() method

//Remove ThreadLocal variable
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //Or linear exploration to clear
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        //If the reference key of the traversal array is equal, it will be cleared directly
        if (e.get() == key) {
            e.clear();
           	//Clean up later according to the current subscript
            expungeStaleEntry(i);
            return;
        }
    }
}

4.ThreadLocal summary

The source code analysis is over here. I wrote it according to my familiarity. What I wrote is not very detailed. The focus is to see more and more breakpoints.

expungeStaleEntry(int staleSlot); Clean the dirty Entry from the staleslot subscript
cleanSomeSlots(int i, int n); The number of times the dirty entry n - > cycle ends after I subscript
Replacestateentry (I) cleans up dirty entries from before and after array position i

In the life cycle of threadLocal, for the memory leakage of threadLocal, the dirty entry with null key will be cleaned up through three methods: expungeStaleEntry, cleansomeslots and replacestateentry

5. Strong reference & weak reference - why use weak reference?

From the above analysis, we know that each thread has its own ThreadLocalMap, which maintains an Entry object. The key is a weak reference pointing to the thread (the thread will be recycled after execution, and the key will not be referenced), and the Object value = v is a strong reference pointing to our storage object instance, that is, there will be a situation when there is no remove:

When the thread execution is completed and the thread declaration cycle ends, the thread will be recycled by GC, and the key reference in the Entry will be null, but value still exists at this time

According to the code analysis, the dirty data of the Entry will be removed every time set, get and remove are performed. Why not remove or memory leakage will occur?

After multi-party data query, if a thread has a long execution link and does not continue to use value in the process of final termination, the strong reference of value will remain all the time, which may lead to memory leakage. Therefore, it is best to call the remove method after use to prevent memory leakage

  • Weak reference list in Java
@Data
static class ReqData {
    String name;
    String reqId;
}   
static class WeakReferenceBean extends WeakReference<ReqData> {
    public WeakReferenceBean(ReqData referent) {
        super(referent);
    }
}
public static void main(String[] args) throws InterruptedException {
    ReqData reqData = new ReqData();
    reqData.setReqId("124578");
    reqData.setName("Weak reference");
    WeakReferenceBean bean = new WeakReferenceBean(reqData);
    System.out.println("before:" + bean.get()); //->Reqdata (name = weak reference, reqId=124578)
    //The instance object is empty and then called GC.
    reqData = null;
    System.gc();
    //Ensure that the recycled virtual machine can see the recycling log by adding the parameter - XX:+PrintGCDetails
    Thread.sleep(2000);
    System.out.println("after:" + bean.get());//->null
}

That is the whole content of this chapter.

Previous: J. Tool classes and principle analysis in U.C (CountDownLatch, Semaphore, cyclicbrier)
Next: This article takes you through the use and source code of Java thread pool

Although the road is endless and faraway, I still want to pursue the truth in the world.

Keywords: Java Multithreading source code

Added by EviL_CodE on Fri, 10 Dec 2021 05:52:14 +0200