ThreadLocal principle and usage scenario

ThreadLocal

ThreadLocal means thread local variable, which is used to solve the problem of accessing shared variables when multiple threads are concurrent.

The so-called shared variables refer to instances, static attributes and arrays in the heap; Access to shared data is controlled by Java's memory model (JMM), which is as follows:

[reference: Art of Java Concurrent Programming P22]

Each thread will have its own local memory. The variables in the heap (that is, the main memory in the figure above) will be copied to the local memory of a replica thread when used by the thread. When the thread modifies the shared variables, they will be written to the main memory through JMM management control.

Obviously, in the multi-threaded scenario, when multiple threads modify shared variables, there will be a thread safety problem, that is, data inconsistency. A common solution is to Lock the code accessing shared variables (synchronized or locked). However, this method costs a lot of performance. At jdk1 In 2, ThreadLocal class is introduced to modify shared variables so that each thread has a separate shared variable, so that the isolation of shared variables between threads can be achieved.

Of course, the usage scenarios of lock and ThreadLocal are different. The specific differences are as follows:

synchronized (lock)ThreadLocal
principleThe synchronization mechanism uses the method of time for space, and only provides a variable for different threads to queue up for access (queuing in the critical area)Space for time is used to provide a copy of variables for each thread, so as to achieve simultaneous access without interference
emphasisSynchronization of accessing resources between multiple threadsIn multithreading, the data between each thread is isolated from each other

1, Use and principle of ThreadLocal

1.1 use

  1. ThreadLocal is generally declared as a static field and initialized as follows:
static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

Where Object is the data of shared variables in the original heap.

For example, a User object needs isolated access between different threads. ThreadLocal can be defined as follows:

public class Test {
    static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}
  1. Common methods
  • set(T value): sets the contents of thread local variables.
  • Get thread's local variable (): get thread's content.
  • remove(): remove thread local variables. Note that in the thread reuse scenario of the thread pool, remove must be called when the thread is executed to avoid the old state of the local variable being saved when the thread is put back into the thread pool.
public class Test {
    static ThreadLocal<User> threadLocal = new ThreadLocal<>();
    
    public void m1(User user) {
        threadLocal.set(user);
    }
    
    public void m2() {
        User user = threadLocal.get();
        // use
        
        // Clear after use
        threadLocal.remove();
    }
}

1.2 principle

So how to save a separate local variable in each Thread? First, what are threads in Java? Yes, it is an instance object of Thread class! The content of the instance member field in an instance object must be unique to this object, so we can also save the ThreadLocal Thread local variable as a member field of the Thread class. This member field is:

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

Is a Map object defined in ThreadLocal, which saves all local variables in the thread. The Entry in ThreadLocalMap is defined as follows:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    // key is a ThreadLocal object, and v is the object we want to isolate between threads
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Both ThreadLocalMap and Entry are defined in ThreadLocal.

Principle of ThreadLocal::set method

The source code of the set method is as follows:

public void set(T value) {
    // Get current thread
    Thread t = Thread.currentThread();
    // Gets the threadLocals field of the current thread
    ThreadLocalMap map = getMap(t);
    // Determine whether the threadlocales of the thread is initialized
    if (map != null) {
        map.set(this, value);
    } else {
        // If not, create a ThreadLocalMap object for initialization
        createMap(t, value);
    }
}

The source code of the createMap method is as follows:

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

map. The source code of the set method is as follows:

/**
* Set the association relationship of ThreadLocal in the map
* set The quick selection method in the get method is not used, because it is as common to create a new entry in the set as to replace the contents of the old entry,
* In the case of replacement, the fast path usually fails (translation of official comments)
*/
private void set(ThreadLocal<?> key, Object value) {
    // In the map, the entry [] data is used to keep all entry instances
    Entry[] tab = table;
    int len = tab.length;
    // Returns the next hash code. The generation process of hash code is related to the magic number 0x61c88647
    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) {
            // Replace the old value if it already exists
            e.value = value;
            return;
        }
        if (k == null) {
            // During setting, clean up the contents of the hash table that are empty and maintain the nature of the hash table
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // Expansion logic
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

Principle of Thread::get method

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // Get the Entry object retained in the Map corresponding to ThreadLocal
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // Gets the value corresponding to the ThreadLocal object
            T result = (T)e.value;
            return result;
        }
    }
    // When the map has not been initialized, create a map object, set null, and return null
    return setInitialValue();
}

Principle of ThreadLocal::remove() method

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    // The key is removed directly
    if (m != null) {
        m.remove(this);
    }
}

The class structure system of ThreadLocalMap is as follows:

1.3 ThreadLocal design

  1. In the early design of JDK, each ThreadLocal has a map object, with the thread as the key of the map object and the variable to be stored as the value of the map, but this is not the case now.
  2. After JDK8, each Thread maintains a ThreadLocalMap object. The key of this Map is the ThreadLocal instance itself, and the value is the variable to be isolated from the stored value. It is generic. The specific process is as follows:
    • Each Thread thread has a Map (ThreadLocalMap::threadlocals);
    • The Map stores the ThreadLocal object (key) and the variable copy (value) of the thread;
    • The map inside the Thread is maintained by ThreadLocal, which is responsible for obtaining and setting variable values from the map;
    • For different threads, each time a copy value is obtained, other threads cannot obtain the copy value of the current thread, forming an isolation between data.

The benefits of design after JDK8 are:

  • The number of entries stored in each Map becomes less. In the actual development process, the number of Thread Local is often less than the number of threads. Reducing the number of entries can reduce hash conflicts.
  • When the Thread is destroyed, the ThreadLocalMap will also be destroyed to reduce memory usage. The early ThreadLocal will not be destroyed automatically.

Benefits of using ThreadLocal

  1. Save the data bound by each thread and obtain it directly where necessary to avoid the code coupling problem caused by directly passing parameters;
  2. The data of each thread is isolated from each other, but it has concurrency, so as to avoid the performance loss caused by synchronization.

2, ThreadLocal memory leak problem

Memory leak problem: refers to that the heap memory dynamically allocated in the program is not released or cannot be released for some reason, resulting in a waste of system memory, resulting in serious consequences such as slow program running speed or system crash. Memory leak accumulation will cause memory overflow.

The memory leakage of ThreadLocal is generally considered to be related to the Entry object. It can be seen from the above Entry definition that ThreadLocal::Entry is modified by weak reference** The JVM will clean up the objects decorated by weak references in the next garbage collection** In this way, the life cycle of ThreadLocal and the life cycle of threads can be unbound. However, it is not true that memory leakage occurs when weak references are used. Consider the following processes:

  1. Use strong references

When ThreadLocal Ref is recycled, because strong references are used in the Entry, there is a reference chain reaching the Entry when the Current Thread still exists, so the content of ThreadLocal cannot be cleared, and the value of the Entry will also be retained; In other words, even if strong references are used, memory leakage will still occur.

  1. Use weak references

When ThreadLocal Ref is recycled, because weak references are used in the Entry, the ThreadLocal object will be cleared in the next garbage collection. At this time, the key in the Entry = null. However, due to the strong reference of Current Thread Ref in ThreadLocalMap, the value in Entry cannot be cleared. There is still a memory leak.

It can be found that the problem of memory leakage caused by using ThreadLocal is that the life cycle of ThreadLocalMap is consistent with that of Thread. If the Entry object is not cleared manually, it may cause memory leakage. Therefore, we need to manually remove the Entry object after each use.

So why use weak references?

There are two ways to avoid memory leakage: after using ThreadLocal, call its remove method to delete the corresponding Entry, or after using ThreadLocal, the current Thread will end. The second method cannot be implemented when using Thread pool technology.

Therefore, you usually call the remove method manually. Calling the remove method, weak references and strong references will not cause memory leakage. The reasons for using weak references are as follows:

In the set/getEntry of ThreadLocalMap, the key will be judged. If the key is null, the value will also be set to null. In this way, even if you forget to call the remove method, when ThreadLocal is destroyed, the content of the corresponding value will be cleared. One more layer of protection!

Summary: there are two memory leaks: ThreadLocal and Value in Entry; The safest thing is to call the remove method in time!!!

3, Application scenario of ThreadLocal

Scenario 1: explicit transfer of substitution parameters in reentry method

If we need to call other methods in our business methods, and other methods need to use the same object, we can use ThreadLocal instead of parameter passing or static global variables. This is because the use of parameter passing results in high code coupling, and it is unsafe to use static global variables in a multithreaded environment. When the object is wrapped in ThreadLocal, it can be guaranteed to be unique in the thread and isolated from other threads.

For example, in the annotation of Spring's @ Transaction transaction Transaction declaration, ThreadLocal is used to save the current Connection object, so as to avoid using different Connection objects in different methods called this time.

Scenario 2: global storage of user information

You can try to use ThreadLocal instead of Session. When the user wants to access the interface that needs authorization, you can now store the user's Token in ThreadLocal in the interceptor; After that, any user who needs user information in this access can directly fetch data from ThreadLocal. For example, customize the class AuthHolder for obtaining user information:

public class AuthNHolder {
    private static final ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>();
    public static void map(Map<String,String> map){
        threadLocal.set(map);
    }
    // Get user id
    public static String userId(){
        return get("userId");
    }
    // Obtain the corresponding information according to the key value
    public static String get(String key){
        Map<String,String> map = getMap();
        return map.get(key);
    }
    // Empty ThreadLocal after use
    public static void clear(){
        threadLocal.remove();
    }
}

Note: refer to the blog https://cloud.tencent.com/developer/article/1636025 . The value encapsulated in ThreadLocal is just an example. It can be changed according to specific business requirements.

Scenario 3: solve thread safety problems

Depending on the characteristics of ThreadLocal itself, ThreadLocal can be used to encapsulate variables that need thread isolation.

4, Summary

  1. ThreadLocal is more like a layer of packaging for other types of variables. Through the packaging of ThreadLocal, this variable can be isolated between threads and shared globally by the current thread.

  2. The isolation of threads and the global sharing of variables benefit from the threadlocals field in each Thread class. (see threads in Java abstractly from the perspective of class instance objects!!!)

  3. The Key of the Entry in ThreadLocalMap may leak memory regardless of whether a weak reference is used. Memory leakage is mainly caused by ThreadLocal object and Value object in Entry. Therefore, make sure to remove the Entry after each use!

Keywords: Java Back-end

Added by cheekychop on Sun, 02 Jan 2022 19:01:17 +0200