Detailed explanation of ThreadLocal in Java

1, Introduction to ThreadLocal

Concurrency is easy to occur when multiple threads access the same shared variable, especially when multiple threads write to a variable. In order to ensure thread safety, general users need to take additional synchronization measures when accessing shared variables to ensure thread safety. ThreadLocal is a way to avoid thread insecurity in multi-threaded access in addition to locking. After creating a variable, if each thread accesses its own variables, there will be no thread insecurity.

ThreadLocal is provided by the JDK package. It provides thread local variables. If a ThreadLocal variable is created, each thread accessing this variable will have a copy of this variable. In the actual multi-threaded operation, the variables in its own local memory are operated, so as to avoid thread safety problems, as shown in the following figure

2, ThreadLocal is easy to use

In the following example, we open two threads, set the values of local variables inside each thread, and then call the print method to print the value of the current local variables. If you call the local variable's remove method after printing, the variables in the local memory will be deleted. The code is shown below.

package test;

public class ThreadLocalTest {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str) {
        //Prints the value of a local variable in local memory in the current thread
        System.out.println(str + " :" + localVar.get());
        //Clear local variables in local memory
        localVar.remove();
    }

    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //Set the value of the local variable in thread 1
                localVar.set("localVar1");
                //Call print method
                print("thread1");
                //Print local variables
                System.out.println("after remove : " + localVar.get());
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //Set the value of the local variable in thread 1
                localVar.set("localVar2");
                //Call print method
                print("thread2");
                //Print local variables
                System.out.println("after remove : " + localVar.get());
            }
        });

        t1.start();
        t2.start();
    }
}

Here are the results after running:

3, Implementation principle of ThreadLocal

The following is the class diagram structure of ThreadLocal. It can be seen from the figure that there are two variables threadLocals and inheritableThreadLocals in the Thread class, both of which are variables of ThreadLocal internal class ThreadLocalMap type. We can find that it is actually similar to a HashMap by looking at the internal ThreadLocalMap. By default, both variables in each Thread are null, threads are created only when they first call ThreadLocal's set or get methods (we'll see the source code of these two methods later). In addition, different from what I think, the local variable of each Thread is not stored in the ThreadLocal instance, but in the threadlocals variable of the calling Thread (as mentioned earlier, this variable is a variable of Thread class). In other words, the local variable of ThreadLocal type is stored in the specific Thread space. It is equivalent to a tool shell for loading local variables. The value is added to the threadlocales of the calling Thread through the set method. When the calling Thread calls the get method, the variable can be taken out of its threadlocales. If the calling Thread does not terminate, the local variable will always be stored in its threadlocales. Therefore, when the local variable is not used, you need to call the remove method to delete the unused local variable from threadlocales. Let's see how ThreadLocal works by looking at ThreadLocal's set, get and remove methods

1. set method source code

public void set(T value) {
    //(1) Gets the current thread (caller thread)
    Thread t = Thread.currentThread();
    //(2) Take the current thread as the key value to find the corresponding thread variable and find the corresponding map
    ThreadLocalMap map = getMap(t);
    //(3) If the map is not null, the local variable is added directly. The key is the this reference of the currently defined ThreadLocal variable, and the value is the added local variable value
    if (map != null)
        map.set(this, value);
    //(4) If the map is null, it means that the corresponding map needs to be created for the first time
    else
        createMap(t, value);
}

In the above code, (2) call the getMap method to obtain the threadlocales corresponding to the current thread (refer to the above illustration and text description). The method code is as follows

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; //Gets the thread's own variable threadLocals and is bound to the member variable threadLocals of the current calling thread.
}

If the return value of calling the getMap method is not null, set the value value directly to threadLocals (the key is the current thread reference and the value is a local variable); If the getMap method returns null, it means that the set method is called for the first time (as mentioned earlier, the default value of threadlocales is null, and the map will be created only when the set method is called). At this time, you need to call the createMap method to create threadlocales, as shown below

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

The createMap method not only creates threadlocales, but also adds the local variable value to be added to threadlocales.

2. get method source code

In the implementation of the get method, first get the current caller thread. If the threadLocals of the current thread is not null, it will directly return the local variable value of the current thread binding, otherwise execute the setInitialValue method to initialize the threadLocals variable. In the setinitialvalue method, similar to the implementation of the set method, it judges whether the threadlocals variable of the current thread is null. If yes, add the local variable (the added value is null because it is initialized at this time). Otherwise, create the threadlocals variable and the added value is null.

public T get() {
    //(1) Get current thread
    Thread t = Thread.currentThread();
    //(2) Gets the threadLocals variable of the current thread
    ThreadLocalMap map = getMap(t);
    //(3) If the threadLocals variable is not null, you can find the value of the local variable in the map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4) At this point, threadLocals is null. Call this change to initialize the threadLocals variable of the current thread
    return setInitialValue();
}

private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //Get current thread
    Thread t = Thread.currentThread();
    //Take the current thread as the key value to find the corresponding thread variable and find the corresponding map
    ThreadLocalMap map = getMap(t);
    //If the map is not null, the local variable is added directly. The key is the current thread and the value is the added local variable value
    if (map != null)
        map.set(this, value);
    //If the map is null, it means that the corresponding map needs to be created for the first time
    else
        createMap(t, value);
    return value;
}

3. Implementation of remove method

The remove method determines whether the threadLocals variable corresponding to the current thread is null. If it is not null, directly delete the threadLocals variable specified in the current thread

public void remove() {
    //Gets the threadlocales bound by the current thread
     ThreadLocalMap m = getMap(Thread.currentThread());
     //If the map is not null, the local variable of the specified ThreadLocal instance in the current thread is removed
     if (m != null)
         m.remove(this);
 }

4. As shown in the following figure: there is a member variable named threadLocals inside each thread. The type of the variable is ThreadLocal.ThreadLocalMap (similar to a HashMap), where the key is the this reference of the currently defined ThreadLocal variable, and the value is the value we set using the set method. The local variables of each thread are stored in its own local memory variable threadLocals. If the current thread does not die, these local variables will always exist (which may lead to memory overflow). Therefore, you need to remove them after use.

4, ThreadLocal does not support inheritance

After the same ThreadLocal variable is set in the parent thread, it cannot be obtained in the child thread. (threadLocals is the local variable corresponding to the current calling thread, so the two can not be shared naturally).

package test;

public class ThreadLocalTest2 {

    //(1) Create ThreadLocal variable
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //Add the local variable of the main thread to the main thread
        threadLocal.set("mainVal");
        //Create a new child thread
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Local variable values in child threads:"+threadLocal.get());
            }
        });
        thread.start();
        //Outputs the value of the local variable in the main thread
        System.out.println("mainx Local variable value in thread:"+threadLocal.get());
    }
}

5, InheritableThreadLocal class

The ThreadLocal class mentioned above cannot provide the child thread to access the local variables of the parent thread, while the InheritableThreadLocal class can do this function. The following is the source code of this class

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

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

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

As can be seen from the above code, the inheritablethreadlocal class inherits the ThreadLocal class and rewrites the childValue, getmap and createMap methods. When the createMap method is called (it needs to be called when the map obtained when the current thread calls the set method is null), it creates inheritablethreadlocal instead of threadlocals. Similarly, the getMap method returns when the caller thread invokes the get method, not threadLocals, but inheritableThreadLocal.

Let's see when the overridden childValue method is executed and how to let the child Thread access the local variable value of the parent Thread. Let's start with the Thread class

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    //Judge the legitimacy of the name
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;
    //(1) Get current thread (parent thread)
    Thread parent = currentThread();
    //Security verification
    SecurityManager security = System.getSecurityManager();
    if (g == null) { //g: Current thread group
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    g.checkAccess();
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g; //Set as current thread group
    this.daemon = parent.isDaemon();//Daemon thread or not (same as parent thread)
    this.priority = parent.getPriority();//Same priority as parent thread
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    //(2) If the inheritableThreadLocal of the parent thread is not null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //(3) Set inheritablethreadlocales in the child thread to inheritablethreadlocales of the parent thread
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;

    tid = nextThreadID();
}

In the init method, first (1) gets the current thread (Fu Xiancheng), then (2) determines whether the inheritableThreadLocals of the current parent thread is null, then calls createInheritedMap to create a new ThreadLocalMap variable using the inheritableThreadLocals of the parent thread as the constructor parameter, and assigns it to the child thread. The following are the construction methods of createinheritedmap method and threadlocalmap

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                //Call overridden method
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

Assign the value of the inheritablethreadlocals member variable of the parent thread to the new ThreadLocalMap object in the constructor. The inheritablethreadlocals assigned to the child thread after return. In short, the inheritablethreadlocals class saves the local variables in the inheritablethreadlocals variable of a specific thread by overriding the getMap and createMap methods. When the thread sets the variables through the set or get methods of the inheritablethreadlocals instance, the inheritablethreadlocals variable of the current thread will be created. When the parent thread creates a child thread, the constructor in ThreadLocalMap will copy the variables in the inheritablethreadlocals of the parent thread to the inheritablethreadlocals variable of the child thread.

6, Memory leakage caused by improper use of ThreadLocal from ThreadLocalMap

1. Basic concepts

First, let's look at the class diagram of ThreadLocalMap. In the previous introduction, we know that ThreadLocal is just a tool class. It provides users with get, set and remove interfaces to operate threadLocals (member variables of the calling thread) that actually store local variables. We also know that threadLocals is a variable of ThreadLocalMap type, Let's take a look at the ThreadLocalMap class. Before that, let's recall the four reference types in Java. The related GC is just a reference to the previous series of articles( JVM related)

① Strong reference: the default reference type in Java. If an object has a strong reference, it will not be used by GC as long as the reference still exists.

② Soft reference: in short, if an object has a weak reference, it will be used before the JVM OOM occurs (that is, the memory is full) , the object will not be GC; the object will not be GC until the JVM is out of memory. The soft reference is used in conjunction with a reference queue. If the object referenced by the soft reference is recycled, the reference will be added to the associated reference queue

③ Weak reference (the key point of the Entry class in ThreadLocalMap is discussed here): if an object has only weak reference, the object will be discarded by the garbage collector GC (the object referenced by the weak reference can only survive until the next GC. When a GC occurs, the object referenced by the weak reference will be recycled regardless of whether the current memory is sufficient or not) . a weak reference is also used in conjunction with a reference queue. If the weakly referenced object is recycled during the garbage collection period, the JVM will add the reference to the reference queue associated with it. If the referenced object can be obtained through the get method of the weak reference, when the referenced object is collected, calling the get method will return null

④ Virtual reference: virtual reference is the weakest of all references. It exists to receive a notification after the object associated with the virtual reference is GC dropped. (the object it points to cannot be obtained through the get method)

2. Analyze the internal implementation of ThreadLocalMap

As we know above, ThreadLocalMap is actually an Entry array, let's first look at the inner class of Entry

/**
 * Is a class inherited from WeakReference. The key actually stored in this class is
 * The weak reference to ThreadLocal and its corresponding value value (the value)
 * Is the value passed through the set method of ThreadLocal)
 * Because it is a weak reference, when the get method returns null, it means that the pit can be referenced
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** value It is bound to ThreadLocal */
    Object value;

    //k: The reference of ThreadLocal is passed to the constructor of WeakReference
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
//Construction method of WeakReference (public class WeakReference < T > extends reference < T >)
public WeakReference(T referent) {
    super(referent); //referent: reference to ThreadLocal
}

//Reference construction method
Reference(T referent) {
    this(referent, null);//referent: reference to ThreadLocal
}

Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

In the above code, we can see that the reference k of the current ThreadLocal is passed to the constructor of WeakReference, so the key in ThreadLocalMap is the weak reference of ThreadLocal. When a thread calls the set method of ThreadLocal to set variables, the ThreadLocalMap of the current thread will store a record, and the key value of this record is the weak reference of ThreadLocal, Va Lue is the value set through set. If the current thread always exists and does not call the remove method of the ThreadLocal, if there are references to ThreadLocal elsewhere at this time, there will be references to ThreadLocal variables and value objects in the ThreadLocalMap in the current thread, which will not be released, resulting in memory leakage.

Considering that this ThreadLocal variable has no other strong dependencies, if the current thread still exists, because the key in the ThreadLocalMap of the thread is a weak reference, the weak reference of the ThreadLocal variable in the ThreadLocalMap of the current thread will be recycled during gc, but the corresponding value still exists, which may cause memory leakage (because at this time, there will be an entry item with null key but non null value in ThreadLocalMap).

Summary:
The key of the Entry in threadlocalmap uses a weak reference to the ThreadLocal object. There is no dependency on ThreadLoca elsewhere. The ThreadLocal object in threadlocalmap will be recycled, but the corresponding will not be recycled. At this time, there may be items with null key but non null value in the Map, which need to be avoided by calling the remove method in time after actual use Memory leak.

Tags: Java Back-end ThreadLocal

Posted on Wed, 03 Nov 2021 14:41:53 -0400 by ReignFire