"Crouching in a pit can enter a big factory" multi-threaded series - ThreadLocal intensive lecture (Basic + source code)

preface

Me: Good morning, dog leftover son. I haven't seen you for two days. I thought you changed jobs and joined A-San's company!

Dog leftover son: you're eating spicy farts. It's exciting. If you don't finish the Gie tutorial, I'll jump in any slot.

Me: Yo hoo, where have you been these two days?

Dog leftover son: I'm not practicing in seclusion. I just left the customs!

Me: After analysis, it's a stupid dog.

 

text

Me: dog, dog, can you tell me what ThreadLocal is? Last time Ah Xiang asked me, I didn't answer. Was it embarrassing?

Don't be afraid, brother dog will teach you.

ThreadLocal is mainly used to provide thread local variables, that is, this variable is only visible to the current thread.

ThreadLocal maintains a data structure inside it (similar to HashMap). When a variable needs to be used, it will create a variable copy in each thread, and then operate the copy through set() and get(), so as to realize the data isolation between threads.

Me: it doesn't sound very difficult! Where do we usually use ThreadLocal?

Although it sounds very simple, it's not so simple to really master it. Pour a cup of tea, order a compliment, and listen to me carefully. Here are some common cases.

1) SimpleDateFormat time format conversion:

Dog brother can guarantee that 100% of the children have used SimpleDateFormat to format the time. Generally, we will package a tool class. The following example is used to print the format time of 1 to 1000 seconds.

public class ThreadLocalDemo {
    //Create a fixed size thread pool
    public static ExecutorService executorService = Executors.newFixedThreadPool(5);
    
    public static void main(String[] args) {

        for (int i = 0; i <= 1000; i++) {
            int time = i ;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    //Call time class static method
                    String formatTime = TimeUtil.getSecond(time);
                    System.out.println(formatTime);
                }
            });
        }
        executorService.shutdown(); 
    }
}

//Time tool class
class TimeUtil {
    static SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-DD HH:mm:ss");

    public static String getSecond(int count){
        Date date = new Date(1000*count);
        return sdf.format(date);
    }
}

submit: add thread task; shutdown: stop the thread pool;

A simple understanding of thread pool here is to always maintain several threads running. When a task arrives, it will be processed immediately. If there are too many tasks to be processed, it will be stuffed into the queue and continue to be executed after the task being executed is completed.

At this time, we will find a strange scene. The time variable is clearly a natural number from 0 to 1000, and there will be no duplicate numbers. Why will the same formatting result be printed here. Do you think of those methods written before and feel cold on your back.

The reason for this is that all threads share the same simpleDateFormat object, which leads to thread safety problems. Smart little partners can certainly think that we can lock the key code with synchronized to ensure the thread safety of the result.

public static String getSecond(int count){
    Date date = new Date(1000*count);
    String result = null;
    synchronized(ThreadLocalDemo.class){
        result = sdf.format(date);
    }
    return result;
}

In this way, our purpose can be achieved, and there will be no accidents in the results, but doing so will lead to only one thread executing time formatting (execution serialization) at the same time, which will seriously affect the performance of the program and cannot be tolerated in the case of high concurrency.

At this time, our protagonist ThreadLocal will appear on the stage. Here, look at the code directly. The main method remains unchanged and only the tool class is modified.

class TimeUtil {
    public static String getSecond(int count){
        Date date = new Date(1000*count);
        //Get SimpleDateFormat using get
        SimpleDateFormat sdf =  ThreadLocalUtils.simpleDateFormatThreadLocal.get();
        return sdf.format(date);
    }
}

//Create ThreadLocal
class ThreadLocalUtils {
   public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new 			ThreadLocal<SimpleDateFormat>(){
       @Override
       protected SimpleDateFormat initialValue() {
           return new SimpleDateFormat("YYYY-MM-DD HH:mm:ss");
       }
   };
}

At this time, no matter how it is executed, the same result will not appear, indicating that the object created with ThreaLocal can ensure the thread.

2) Avoid passing some parameters

Yesterday 618, I don't know if you have cut your hands. Anyway, I have no hands to cut. I just want to live well. Here we think about a question, when you place an order, how does the background program deal with it? In fact, the background program needs to deal with many processes, including user information query, coupon query, receiving address query, message notification and so on. Because the user information may be used in each step. If the user information is passed layer by layer as parameters, it will lead to high code coupling and very bloated, which is not conducive to maintenance.

Some little friends may think that I don't need your ThreadLocal. I write a static map collection for storage. Can't I save it?

When multiple threads access the same variable, we know that there will be thread safety problems. If you use a collection of thread safety types (such as ConCurrentHashMap) or lock directly, it will affect the execution performance of the program, which is the same as the performance problems caused by the use of synchronized code block modification above.

Therefore, we can conclude that during the life cycle of a thread, the set() method of ThreadLocal can be used to store the private variable of the thread and obtain it through get() when the variable is needed. The content of this variable is independent in different threads, which avoids the trouble of multi-level parameter transmission without losing performance.

public class ThreadLocalDemo2 {
    public static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        User user = new User("flower Gie");
        ThreadLocalInfo.userThreadLocal.set(user);
        //1. Call the method of obtaining address
        new AddressService().getAddress();
    }
}
class AddressService{
    public void getAddress(){
        User user = ThreadLocalInfo.userThreadLocal.get();
        System.out.println("According to user information"+user.getUserName()+"Get user address");
        //2. Call coupon method
        new TicketService().getTicket();
    }
}

class TicketService{
    public void getTicket(){
        User user = ThreadLocalInfo.userThreadLocal.get();
        System.out.println("According to user information"+user.getUserName()+"Get user coupons");
        //3. Call send message
        new MessageService().sendMessage();
    }
}
class MessageService{
    public void sendMessage(){
        User user = ThreadLocalInfo.userThreadLocal.get();
        System.out.println("According to user"+user.getUserName()+"send message");
    }
}

class User {
    String userName;

    public User(String userName) {
        this.userName = userName;
    }

    public String getUserName() {
        return userName;
    }
}
//Create ThreadLocal variable
class ThreadLocalInfo {
   public static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
}

The explanation of the code is really fragrant and clear. In the three steps of obtaining the address, obtaining the coupon and sending the message, we do not transfer the user object layer by layer, but we can get the user object content. Even if the subsequent requirements or processes are adjusted, we can easily deal with it.

Me: brother dog, I know the usage and function now. Can you tell me the implementation principle?

OK, let's take a look at this picture first to understand the relationship among Thread, ThreadLocal and ThreadLocalMap.

It can be clearly seen from the figure that each Thread will have a ThreadLocalMap object, and each ThreadLocalMap contains multiple threadlocales.

Me: This is your own drawing How do I know if it's bullshit!

It took Gie a few days to be smart. Let's take a look at the source code. ThreadLocal has four important methods:

public T get() {}
public void set(T value) {}
protected T initialValue() {}

First, let's take a look at the initialValue() method we rewritten in the first scenario:

protected T initialValue() {
    return null;
}

Obviously, if we don't actively rewrite the initializalvalue () method, it will return a null value.

Next, let's look at the set method:

public void set(T value) {
    //1. Get the current thread
    Thread t = Thread.currentThread();
    //2. Get ThreadLocalMap object
    ThreadLocalMap map = getMap(t);
    //3. If the map exists, the current ThreadLocal object will be stored in the map as a key
    //this: is the current ThreadLocal
    if (map != null)
        map.set(this, value);
    else
        //4. When the map does not exist, it is created
        createMap(t, value);
}

The second step above has a ThreadLocalMap. What is this? In fact, we can find the result in the Thread class, which is an internal class in the Thread. Here's the key code. ThreadLocalMap uses the key value array Entry[] table to store data. Entry can be compared to a map, where the key refers to ThreadLocal; Value is the content to be saved, such as SimpleDateFormat and user.

ThreadLocalMap is the gray part in the figure above

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
      private Entry[] table;
    ....ellipsis
}

Then look at the contents of getMap. It will return the threadLocals object of the current thread. Therefore, when initialValue() is not overridden for initialization, the first call to set() method getMap will return a null, and then createMap it.

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// Thread.java 
// threadLocals defaults to null
ThreadLocal.ThreadLocalMap threadLocals = null;

The createMap operation is also relatively simple. It is used to create a ThreadLocalMap object and assign it to the threadLocals variable of the current thread.

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

After reading the above source code, the get() method is very easy to understand. First look at the code in step 3. If the set() method has been called before (the threadLocals object has not been initialized), the current ThreadLocal object will be used as the key to obtain the value value saved in threadLocals in advance.

public T get() {
    //1. Get the current thread
    Thread t = Thread.currentThread();
    //2. Get the threadLocalMap of the current thread
    ThreadLocalMap map = getMap(t);
    //3. If ThreadLocal has been initialized
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //4. If ThreadLocal is not initialized, call initialValue() to initialize
    return setInitialValue();
}

But what does the last sentence do? In fact, the method of delayed loading is used here. When we rewrite the initialValue() method, it does not initialize immediately. Instead, we wait until the first query to execute the setInitialValue() method for initialization.

private T setInitialValue() {
    //Overridden initialValue method
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

See here, do you basically understand the relationship and process of Thread, ThreadLocal and ThreadLocalMap in the figure.

Me: although it's very wordy, it's really detailed!

Here's what I want to say. Every time I say so much, my hands are sour and my mouth is tired, but I'm very worried that if I ignore some details, many little partners may not understand. Therefore, the little partners with more disadvantages can jump to see and ignore some details. If they are novice partners, they look easier and easier to understand.

Me: Gouzi is very attentive. Flower Gie likes you here. Since ThreadLocal has so many advantages, is there any defect in it?

Everything has two sides. ThreadLocal must have some problems to pay attention to, that is, memory leakage

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

As in the above code, value = v , indicates that the value we set in ThreadLocal and ThreadLocalMap are strong references. Combined with the above figure, we can get the reason for memory leakage: because the life cycle of ThreadLocalMap is as long as that of Thread, if the Thread is not destroyed, ThreadLocal will survive all the time. If the corresponding key is not manually deleted, memory leakage will occur.

For example, in the scenario of thread pool, because threads always survive, using ThreadLocal at this time will lead to memory leakage. If you want to avoid memory leakage, you need to manually remove()!

Me: dog, I'm stunned at the effect of your two-day cultivation. Let's summarize it for my friends.

ThreadLocal can be summarized as follows:

  • In the life cycle of the thread, the object can be easily obtained;
  1. effect:

    • Each thread can have its own independent object, which can be isolated from other threads;
    • In the life cycle of the thread, the object can be easily obtained;
  2. advantage:

    • Thread safety can be achieved without locking
    • Efficient use of memory and cost savings
    • There is no need to transfer parameters layer by layer to realize code decoupling
  3. Scene selection:

    • For the tool class, when all threads share one instance, the ThreadLocal variable is initialized when it is created;
    • When different threads have different objects, set() method can be used flexibly.
  4. Principle summary:

    • ThreadLocalMap is the internal class of Thread, and each Thread maintains a reference to ThreadLocalMap
    • initialValue() and set() set key value pairs are essentially the same, and both of them call map set(this,value)
    • ThreadLocal itself does not store values, it just gets values from ThreadLocalMap as a key.

summary

The purpose of using ThreadLocal is not to solve the problem of concurrent or shared variables, but to have its own variables in the current thread and realize thread data isolation.

Finally, Xiaobian sorted out some learning materials during the learning process, which can be shared with friends who are software test engineers to exchange and learn from each other, If necessary, you can join my learning exchange group 323432957 or add dingyu-002 to get the learning materials of Python automatic test development and Java automatic test development for free (including the architecture materials of multiple knowledge points such as function test, performance test, python automation, Java automation, test development, interface test, APP test, etc.)

Keywords: Python Java Programming Big Data Multithreading

Added by leony on Thu, 27 Jan 2022 19:18:16 +0200