In-depth analysis of ThreadLocal usage scenarios, implementation principles, and design ideas

Preface

ThreadLocal can be used to store local data of threads to isolate thread data.

Improper use of ThreadLocal may lead to memory leaks. Troubleshooting memory leaks requires not only familiarity with the JVM, making good use of various analysis tools, but also laborious work.

If you can understand its principle and use it correctly, it will not cause various accidents.

This article will analyze ThreadLocal from the aspects of usage scenarios, implementation principles, memory leaks, design ideas, etc., and also talk about InheritableThreadLocal.

ThreadLocal usage scenarios

What is context?

For example, when a thread processes a request, the request will go through the MVC process. Since the process is very long, it will go through many methods. These methods can be called contexts.

ThreadLocal is used to store commonly used data, store session information, store thread local variables, etc. in the context.

For example, use an interceptor to obtain the logged-in user information through the token in the request before processing the request, and store the user information in ThreadLocal, so that the user information can be obtained directly from ThreadLocal during subsequent request processing.

If the thread will be reused, in order to avoid data confusion, the data should be deleted after use (after processing by the interceptor)

Commonly used methods of ThreadLocal are: set(), get(), remove()corresponding to storage, acquisition and deletion respectively.

ThreadLocal can be placed in a tool class for easy use

public class ContextUtils {
    public static final ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal();
}

Interceptor pseudocode

//执行前 存储
public boolean postHandle(HttpServletRequest request)  {
    //解析token获取用户信息
	String token = request.getHeader("token");
	UserInfo userInfo = parseToken(token);   
	//存入
	ContextUtils.USER_INFO_THREAD_LOCAL.set(userInfo);
    
    return true;
}


//执行后 删除
public void postHandle(HttpServletRequest request)  {
    ContextUtils.USER_INFO_THREAD_LOCAL.remove();
}

when using it

//提交订单
public void orderSubmit(){
    //获取用户信息
    UserInfo userInfo = ContextUtils.USER_INFO_THREAD_LOCAL.get();
    //下单
    submit(userInfo);
    //删除购物车
    removeCard(userInfo);
}

In order to better use ThreadLocal, we should understand its implementation principles and avoid accidents caused by improper use.

ThreadLocalMap

Thread There are two fields in the thread that store ThreadLocal's internal class ThreadLocalMap

public class Thread implements Runnable {    
    
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

threadLocals is used to implement ThreadLocal

inheritableThreadLocals is used to implement InheritableThreadLocal (inheritable ThreadLocal will be discussed later)

image.png

The implementation of ThreadLocalMap is a hash table, and its internal class Entry is the node of the hash table. The hash table ThreadLocalMap is implemented by the Entry array.

public class ThreadLocal<T> {
    //,,,
	static class ThreadLocalMap {
        //...
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

The Key in the node construction is ThreadLocal, and the Value is the value that needs to be stored.

At the same time, the node inherits weak references. Through generics and constructors, you can know that it sets ThreadLocal to a weak reference.

Students who don’t understand weak references can check out this article: An in-depth introduction to JVM (14) Memory overflows, leaks and references)

image.png

set

In the method of storing data

image.png

Get ThreadLocalMap, if not, initialize ThreadLocalMap (lazy loading)

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
   
    if (map != null) {
        //添加数据
        map.set(this, value);
    } else {
        //没有就初始化
        createMap(t, value);
    }
}

createMap

Create a ThreadLocalMap and assign it to the threadLocals of the current thread.

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

Create a ThreadLocalMap and initialize an array with a length of 16

	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化数组 16
        table = new Entry[INITIAL_CAPACITY];
        //获取下标
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //构建节点
        table[i] = new Entry(firstKey, firstValue);
        //设置大小
        size = 1;
        //设置负载因子
        setThreshold(INITIAL_CAPACITY);
   }

ThreadLocalMap.set

Obtain the subscript through hashing. When a hash conflict occurs, traverse the hash table (no longer using the chain address method) until there is no node at the position and then build.

If there is a node during the traversal, the key will be taken out according to the node for comparison. If it is, it will be overwritten. If the node does not have a key, it means that the ThreadLocal of the node has been recycled (expired). In order to prevent memory leaks, the node will be cleaned.

Finally, it will check whether there are expired nodes in other locations, clean them up, and check for expansion.

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;
         e = tab[i = nextIndex(i, len)]) {
        //获取key
        ThreadLocal<?> k = e.get();
		//key如果存在则覆盖
        if (k == key) {
            e.value = value;
            return;
        }
		//如果key不存在 说明该ThreadLocal以及不再使用(GC回收),需要清理防止内存泄漏
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //构建节点
    tab[i] = new Entry(key, value);
    //计数
    int sz = ++size;
    //清理其他过期的槽,如果满足条件进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

When obtaining the hash value, use the atomic class that increments the hash value, and the step size is the number of increments each time (perhaps after research and testing, to minimize hash conflicts)

	//获取哈希值
    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);
    }

nextIndex is to get the next index, and returns to 0 when the upper limit is exceeded.

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

get

when getting data

image.png

Get the ThreadLocalMap of the current thread, initialize it if it is empty, otherwise get the node

	public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //获取节点
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //初始化(懒加载)
        return setInitialValue();
    }

When obtaining the node, first obtain the subscript based on the hash value, then check the node and compare the key; if it does not match, it means that the key has expired and a memory leak may occur. You need to clean the hash table.

		private Entry getEntry(ThreadLocal<?> key) {
            //获取下标
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果匹配 则返回
            if (e != null && e.get() == key)
                return e;
            else
                //匹配不到 去清理
                return getEntryAfterMiss(key, i, e);
        }

memory leak

During the process of setting and obtaining data, we will judge whether the key has expired. If it expires, clean it up.

In fact, improper use of ThreadLocal can cause memory leaks.

In order to avoid memory leaks caused by improper use, designers try to clean up these expired ThreadLocals in common methods.

As mentioned before, nodes inherit weak references and set the key to weak references in the construction (that is, ThreadLocal).

When ThreadLocal is not used anywhere, the next GC will set the node's key to empty

If the value is no longer used, but because the node Entry (null, value) exists, the value cannot be recycled, resulting in a memory leak.

image.png

Therefore, after using the data, try to use remove to delete it.

In addition, designers will check nodes with empty keys and delete them in common methods such as set, get, and remove to avoid memory leaks.

Design thinking

Why should the key in the entry, that is, ThreadLocal, be set to a weak reference?

Let's first imagine a scenario: threads are often reused in our services, and in some scenarios ThreadLocal is not used for a long time.

If the key and value of the node entry are both strong references, once the ThreadLocal is no longer used, the ThreadLocal is still stored in the node as a strong reference and cannot be recycled, which is equivalent to a memory leak.

After setting ThreadLocal to a weak reference, memory leaks will still occur if the value is no longer used in this scenario. Therefore, in the set, get, and remove methods, nodes with empty keys will be checked and deleted to avoid memory leaks.

Since the value may not be recycled, why not set the value as a weak reference?

Since value stores thread-isolated data, if value is set to a weak reference, when the outer layer does not use the object corresponding to value, it will have no strong reference, and the next time GC is recycled, resulting in data loss.

InheritableThreadLocal

InheritableThreadLocal inherits ThreadLocal and is used to transfer thread variables between parent and child threads.

	public void testInheritableThreadLocal(){
        InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();

        itl.set("main");

        new Thread(()->{
            //main
            System.out.println(itl.get());
        }).start();
    }

As mentioned earlier, another ThreadLocalMap in the thread is used for InheritableThreadLocal.

When creating a thread, if inheritableThreadLocals in the parent thread is not empty, it is passed

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        //....
    
        //如果父线程中inheritableThreadLocals 不为空 则传递
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
    }

Summarize

ThreadLocal is used to isolate data between threads and can store data for use in context. Since threads may be reused, they need to be deleted after use to avoid data confusion.

ThreadLocalMap is stored in the Thread thread. ThreadLocalMap is a hash table that uses the open addressing method to resolve hash conflicts. The node storage Key is ThreadLocal and the Value stores the data to be stored by the thread.

The node inherits the weak reference and sets ThreadLocal as a weak reference. This will cause the ThreadLocal to be recycled next time when the GC is no longer used. At this time, the Key will be empty. If the Value is no longer used, but the node is not deleted, it will cause value is used, causing a memory leak

In the common methods of ThreadLocal such as set, get, remove, etc., while traversing the array, go back and delete the expired nodes (the key is empty) to avoid memory leaks.

If ThreadLocal is set to a strong reference, memory leaks will occur when ThreadLocal is no longer used; when ThreadLocal is set to a weak reference, although memory leaks may also occur, these data can be checked and cleaned up in common methods; if value is set into a weak reference, data loss will occur when the outer layer does not use the value

InheritableThreadLocal inherits ThreadLocal and is used for ThreadLocal data transfer between parent and child threads.

Finally (don’t do it for free, just press three times in a row to beg for help~)

This article is included in the column " From Point to Line, and from Line to Surface" to build a Java concurrent programming knowledge system in simple terms . Interested students can continue to pay attention.

The notes and cases of this article have been included in gitee-StudyJava and github-StudyJava . Interested students can continue to pay attention under stat~

Case address:

Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

If you have any questions, you can discuss them in the comment area. If you think Cai Cai’s writing is good, you can like, follow, and collect it to support it~

Follow Cai Cai and share more useful information, public account: Cai Cai’s back-end private kitchen

This article is published by OpenWrite, a blog that publishes multiple articles !

Lei Jun: The official version of Xiaomi’s new operating system ThePaper OS has been packaged. A pop-up window on the Gome App lottery page insults its founder. The U.S. government restricts the export of NVIDIA H800 GPU to China. The Xiaomi ThePaper OS interface is exposed. A master used Scratch to rub the RISC-V simulator and it ran successfully. Linux kernel RustDesk remote desktop 1.2.3 released, enhanced Wayland support After unplugging the Logitech USB receiver, the Linux kernel crashed DHH sharp review of "packaging tools": the front end does not need to be built at all (No Build) JetBrains launches Writerside to create technical documentation Tools for Node.js 21 officially released
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/6903207/blog/10114997