The best ThreadLocal posture for programmers

1, Common scenarios

1. ThreadLocal is a copy of thread context. One of the most common ways to use ThreadLocal is to implicitly pass parameters to different methods by providing two public methods: set() and get(). For programming specifications, the number of parameters is limited when defining methods. Even in some large factories, the number of method parameters is clearly specified.

2. Thread safety: each thread maintains its own variables to avoid disorder. For example, the thread safety implementation of the connection pool of the commonly used database uses ThreadLocal.

2, Advanced use

Taking parameter passing as an example, how to better use ThreadLocal to realize parameter passing in different methods in the same thread stack. When parameters are passed, there will always be parameter names and parameter values. However, the get() and set() methods provided by ThreadLocal cannot directly meet the requirements of setting parameter names and parameter values. In this case, ThreadLocal needs to be encapsulated. The following code maintains a map object, and then provides setValue(key, value) and getValue(key, value) methods to easily set and obtain parameters; Clean up the parameters where necessary by using remove(key) or clear().

import java.util.HashMap;
import java.util.Map;
 
public class ThreadLocalManger<T> extends ThreadLocal<T> {
 
    private static ThreadLocalManger<Map<String, Object>> MANGER = new ThreadLocalManger<>();
 
    private static HashMap<String, Object> MANGER_MAP = new HashMap<>();
 
    public static void setValue(String key, Object value) {
        Map<String, Object> context = MANGER.get();
        if(context == null) {
            synchronized (MANGER_MAP) {
                if(context == null) {
                    context = new HashMap<>();
                    MANGER.set(context);
                }
            }
        }
        context.put(key, value);
    }
 
    public static Object getValue(String key) {
        Map<String, Object> context = MANGER.get();
        if(context != null) {
            return context.get(key);
        }
        return null;
    }
 
    public static void remove(String key) {
        Map<String, Object> context = MANGER.get();
        if(context != null) {
            context.remove(key);
        }
    }
 
    public static void clear() {
        Map<String, Object> context = MANGER.get();
        if(context != null) {
            context.clear();
        }
    }
}

3, Use vulnerability

Continue to take parameter passing as an example to see the problems and consequences in the use of ThreadLocal. In the actual business function development, in order to improve efficiency, thread pool will be used in most cases, such as database connection pool, RPC request connection pool, MQ message processing pool, background batch job pool, etc; At the same time, a thread (daemon thread) accompanying the whole application life cycle may also be used to realize some functions, such as heartbeat, monitoring, etc. If the thread pool is used, the concurrency in the actual production business is certainly not low, and the threads in the pool will be reused all the time; Once the daemon thread is created, it will live until the application stops. Therefore, in these cases, the life cycle of threads is very long. When using ThreadLocal, you must clean it up, otherwise there will be memory overflow. The following case is used to simulate the memory overflow.

Simulate high concurrency scenarios through an endless loop. Create a thread pool with 10 core threads, 10 maximum threads, 60 second idle time and thread names starting with ThreadLocal demo. In this scenario, there will be 10 threads running. The running content is very simple: generate a UUID, take it as the parameter key, and then set it to the thread copy.

import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;
 
import java.util.UUID;
import java.util.concurrent.*;
 
@Service
public class ThreadLocalService {
 
    ThreadFactory springThreadFactory = new CustomizableThreadFactory("TheadLocal-demo-");
 
    ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(), springThreadFactory);
 
    ExecutorService service = new ThreadPoolExecutor(10, 10, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>());
 
    public Object setValue() {
        for(; ;) {
            try {
                Runnable runnable = new Runnable() {
                    @Override
                    public void run() {
                        String id = UUID.randomUUID().toString();
                        // add
                        ThreadLocalManger.setValue(id, "this is a value");
                        //do something here
                        ThreadLocalManger.getValue(id);
                        // clear()
                        //ThreadLocalManger.clear();
                    }
                };
                executorService.submit(runnable);
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
        return "success";
    }
 
}

The clear() method has been commented out in the above code. Without cleaning up, trigger the program, set the jvm slightly lower, and the following OOM will be reported soon.

java.lang.OutOfMemoryError: GC overhead limit exceeded
Exception in thread "TheadLocal-demo-9" 
Exception in thread "TheadLocal-demo-8" 
Exception in thread "TheadLocal-demo-6" 
Exception in thread "TheadLocal-demo-10" 
Exception in thread "TheadLocal-demo-7" 
java.lang.OutOfMemoryError: GC overhead limit exceeded
Exception in thread "TheadLocal-demo-5" 
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
	at com.intellij.rt.debugger.agent.CaptureStorage.insertEnter(CaptureStorage.java:57)
	at java.util.concurrent.FutureTask.run(FutureTask.java)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded

Serious memory overflow will occur. It can be seen from the following debug screenshot that the UUID s set in accumulate in the memory, gradually increase, and finally burst the memory.

In the actual business scenario, there may be order number, transaction number, serial number, etc. these variables are often the only non repeated and consistent with the UUID in the case. If they are not cleaned up, the application OOM will be unavailable; In the distributed system, it can also lead to the unavailability of upstream and downstream systems, and then lead to the unavailability of the whole distributed system; If this information is often used in network transmission, large messages occupy the network bandwidth and even lead to network paralysis. Therefore, a small detail will put the whole cluster in danger, so how to resolve it reasonably.

4, Final use

The above problem lies in forgetting to clean up, so how to make the cleaning unconscious, that is, there is no problem without cleaning up. The root cause is that the thread does not clean up after running once, so a base class thread can be provided to encapsulate the cleaning at the end of thread execution. The following code. Provide a BaseRunnable abstract base class, which has the following main features.

1. This class inherits Runnable.

2. Implement setArg(key, value) and getArg(key) methods.

2. The rewritten run method is divided into two steps. The first step is to call the abstract method task; The second step is to clean up the thread copy.

With the above three features, it inherits the thread class of BaseRunnable. It only needs to implement the task method, implement the business logic in the task method, and pass and obtain parameters through setArg(key, value) and getArg(key) methods without displaying and cleaning.

public abstract class BaseRunnable implements Runnable {
 
    @Override
    public void run() {
        try {
            task();
        } finally {
            ThreadLocalManger.clear();
        }
    }
 
    public void setArg(String key, String value) {
        ThreadLocalManger.setValue(key, value);
    }
 
    public Object getArg(String key) {
        return ThreadLocalManger.getValue(key);
    }
 
    public abstract void task();
}

Keywords: Java Back-end ThreadLocal

Added by tommyda on Wed, 19 Jan 2022 08:56:15 +0200