44 in Which Practical Production Scenarios Is Thread Local Suitable

44 In Which Practical Production Scenarios Is ThreadLocal Suitable #

This lesson mainly introduces the practical production scenarios where ThreadLocal is suitable.

Before learning about a tool, we should first understand its purpose and the benefits it brings, rather than immediately diving into its API and usage. Otherwise, even if we learn how to use a tool, we won’t know when to use it in which scenarios. So, let’s take a look at what scenarios ThreadLocal is used for.

In typical business development, there are two typical usage scenarios for ThreadLocal.

Scenario 1: ThreadLocal is used to save objects exclusively for each thread, creating a copy for each thread. This allows each thread to modify its own copy without affecting the copies of other threads, ensuring thread safety.

Scenario 2: ThreadLocal is used to independently store information within each thread, making it easier for other methods to obtain this information. Each thread may obtain different information. After a method saves the information, subsequent methods can directly obtain it through ThreadLocal, avoiding the need for parameter passing. This is similar to the concept of global variables.

Typical Scenario 1 #

This scenario is usually used to save thread-unsafe utility classes, and the typical class that needs to be used is SimpleDateFormat.

Scenario Introduction #

In this case, each thread has its own instance copy, and this copy can only be accessed and used by the current thread, so it is equivalent to a local variable within each thread. This is the meaning behind the name ThreadLocal. Because each thread has its own copy instead of sharing it, there is no problem of sharing between multiple threads.

Let’s make an analogy. For example, a restaurant wants to make a dish, but there are 5 chefs working together. This would be chaotic because if one chef has already added salt, and the other chefs don’t know about it, they will each add salt on their own, resulting in a very salty dish. This is similar to the situation with multiple threads, which is thread-unsafe. With ThreadLocal, each chef is responsible for only one dish, and there are 5 dishes in total. This is very clear and there won’t be any issues.

The Evolution of SimpleDateFormat #

1. Both threads need to use SimpleDateFormat

Let’s use an example to illustrate this typical first scenario. Let’s assume that there is a requirement that 2 threads need to use SimpleDateFormat. The code is as follows:

public class ThreadLocalDemo01 {

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {

            String date = new ThreadLocalDemo01().date(1);

            System.out.println(date);

        }).start();

        Thread.sleep(100);

        new Thread(() -> {

            String date = new ThreadLocalDemo01().date(2);

            System.out.println(date);

        }).start();

    }

    public String date(int seconds) {

        Date date = new Date(1000 * seconds);

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

        return simpleDateFormat.format(date);

    }

}

In the above code, it can be seen that two threads each create their own instance of SimpleDateFormat, as shown in the diagram:

img

In this way, there are two threads, so there are two SimpleDateFormat objects, and they do not interfere with each other. This code can run normally, and the output is:

00:01

00:02

2. 10 threads need to use SimpleDateFormat

Let’s say our requirement is upgraded. We not only need 2 threads, but 10 threads. This means that we need 10 threads corresponding to 10 SimpleDateFormat objects. Let’s take a look at the following code:

public class ThreadLocalDemo02 {

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10; i++) {

            int finalI = i;

            new Thread(() -> {

                String date = new ThreadLocalDemo02().date(finalI);

                System.out.println(date);

            }).start();

            Thread.sleep(100);

        }

    }

    public String date(int seconds) {

        Date date = new Date(1000 * seconds);

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

        return simpleDateFormat.format(date);

    }

}

The above code uses a for loop to complete this requirement. The for loop iterates a total of 10 times, creating a new thread each time, and each thread creates a SimpleDateFormat object in the date method. The schematic diagram is as follows:

img

It can be seen that there are a total of 10 threads, corresponding to 10 SimpleDateFormat objects.

The output of the code is:

00:00
00:01
00:02
00:03
00:04
00:05
00:06
00:07
00:08
00:09

3. Requirement changed to use SimpleDateFormat for 1000 threads

However, the threads cannot be created indefinitely, because the more threads there are, the more resources they will consume. Let’s assume we need 1000 tasks, so we can no longer use the method of for loop, but should use a thread pool to achieve thread reuse, otherwise it will consume too much memory and other resources. In this case, we provide the following code implementation solution:

public class ThreadLocalDemo03 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {

            int finalI = i;

            threadPool.submit(new Runnable() {

                @Override

                public void run() {

                    String date = new ThreadLocalDemo03().date(finalI);

                    System.out.println(date);

                }

            });

        }

        threadPool.shutdown();

    }

    public String date(int seconds) {

        Date date = new Date(1000 * seconds);

        SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");

        return dateFormat.format(date);

    }

}

It can be seen that we used a thread pool with 16 threads, and submitted 1000 tasks to this thread pool. Each task does the same thing as before, which is to execute the date method and create a SimpleDateFormat object within this method. One possible output of the program (the result may vary under multi-threading) is:

00:00

00:07

00:04

00:02

...

16:29

16:28

16:27

16:26

16:39

The program output is correct, printing the 1000 times from 00:00 to 16:39 without any duplicate times. We can represent this piece of code graphically as shown below:

img

The left side of the graph represents a thread pool, and the right side represents 1000 tasks. Each task creates a SimpleDateFormat object, which means that there are 1000 SimpleDateFormat objects corresponding to the 1000 tasks.

However, this is unnecessary because creating so many objects has overhead, and the destruction of these objects after use also has overhead. Additionally, having so many objects simultaneously in memory is a waste of memory.

Now let’s optimize it. Since we don’t want so many SimpleDateFormat objects, the simplest solution is to use only one.

4. All threads share one SimpleDateFormat object

We use the following code to demonstrate the case where only one SimpleDateFormat object is used:

public class ThreadLocalDemo04 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);

    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {

            int finalI = i;

            threadPool.submit(new Runnable() {

                @Override

                public void run() {

                    String date = new ThreadLocalDemo04().date(finalI);

                    System.out.println(date);

                }

            });

        }

        threadPool.shutdown();

    }

    public String date(int seconds) {

        Date date = new Date(1000 * seconds);

        return dateFormat.format(date);

    }

}

In the code, everything else remains the same except that we extracted the SimpleDateFormat object and made it a static variable. When needed, we can directly access this static object. It seems that we have eliminated the overhead of creating 1000 SimpleDateFormat objects, but there is a problem. Let’s represent this in a graphical way:

img

From the graph, we can see that we have different threads and the threads will execute their tasks. However, different tasks share the same SimpleDateFormat object, so they all point to the same object. But this will cause a thread safety issue.

5. Thread safety issue and concurrent safety problem

The console will output (the result may vary under multi-threading):

00:04

00:04

00:05

00:04

...

16:15

16:14
16:13

Executing the above code will reveal that the output printed to the console is inconsistent with our expectations. What we expected is that the printed time would not be repeated, but it can be seen that there are duplicates, such as the first and second lines both having “04” seconds. This indicates that there is an error within it.

6. Locking

The reason for the error lies in the fact that the simpleDateFormat object itself is not a thread-safe object and should not be accessed by multiple threads simultaneously. So we thought of a solution, using synchronized to add a lock. So the code is modified as follows:

public class ThreadLocalDemo05 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);

    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {

            int finalI = i;

            threadPool.submit(new Runnable() {

                @Override

                public void run() {

                    String date = new ThreadLocalDemo05().date(finalI);

                    System.out.println(date);

                }

            });

        }

        threadPool.shutdown();

    }

    public String date(int seconds) {

        Date date = new Date(1000 * seconds);

        String s = null;

        synchronized (ThreadLocalDemo05.class) {

            s = dateFormat.format(date);

        }

        return s;

    }

}

It can be seen that the synchronized keyword is added in the date method to lock the invocation of simpleDateFormat.

The result of running this code (under multithreading, the result may not be unique):

00:00

00:01

00:06

...

15:56

16:37

16:36

This result is normal and does not contain duplicate times. However, because we use the synchronized keyword, it will enter a queued state and multiple threads cannot work at the same time, which greatly reduces overall efficiency. Is there a better solution?

We want to achieve the effect of not wasting too much memory and ensuring thread safety at the same time. After thinking about it, we can let each thread have its own simpleDateFormat object to achieve this goal, so that we can have the best of both worlds.

7. Using ThreadLocal

So, in order to achieve this goal, we can use ThreadLocal. The example code is as follows:

public class ThreadLocalDemo06 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {

            int finalI = i;

            threadPool.submit(new Runnable() {

                @Override

                public void run() {

                    String date = new ThreadLocalDemo06().date(finalI);

                    System.out.println(date);

                }

            });

        }

        threadPool.shutdown();

    }

    public String date(int seconds) {

        Date date = new Date(1000 * seconds);

        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();

        return dateFormat.format(date);

    }

}

class ThreadSafeFormatter {

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {

        @Override

        protected SimpleDateFormat initialValue() {

            return new SimpleDateFormat("mm:ss");

        }

    };

}

In this code, we use ThreadLocal to help each thread generate its own simpleDateFormat object. For each thread, this object is unique. But at the same time, this object will not be created too many times, there are only 16 in total, because there are only 16 threads.

The result of running the code (under multithreading, the result may not be unique):

            User user = new User();
            ContextHolder.setUser(user);
            new Service2().service2();
        }
    }

    class Service2 {

        public void service2() {
            User user = ContextHolder.getUser();
            new Service3().service3();
        }
    }

    class Service3 {

        public void service3() {
            User user = ContextHolder.getUser();
            new Service4().service4();
        }
    }

    class Service4 {

        public void service4() {
            User user = ContextHolder.getUser();
            // do something with user info
        }
    }

    class ContextHolder {
        private static ThreadLocal<User> threadLocal = new ThreadLocal<>();

        public static User getUser() {
            return threadLocal.get();
        }

        public static void setUser(User user) {
            threadLocal.set(user);
        }
    }

    class User {
        // user information
    }
        User user = new User("LaGou Education");

        UserContextHolder.holder.set(user);

        new Service2().service2();

    }

}

class Service2 {

    public void service2() {

        User user = UserContextHolder.holder.get();

        System.out.println("Service2 got the username: " + user.name);

        new Service3().service3();

    }

}

class Service3 {

    public void service3() {

        User user = UserContextHolder.holder.get();

        System.out.println("Service3 got the username: " + user.name);

        UserContextHolder.holder.remove();

    }

}

class UserContextHolder {

    public static ThreadLocal<User> holder = new ThreadLocal<>();

}

class User {

    String name;

    public User(String name) {

        this.name = name;

    }

}

In this code, we can see that we have a UserContextHolder which holds a ThreadLocal object. When calling the method in Service1, we store the user object in it. Later on, when we call other methods, we can directly retrieve the object from it using the get method. There’s no need to pass parameters layer by layer, which is very elegant and convenient.

The result of running the code is:

Service2 got the username: LaGou Education

Service3 got the username: LaGou Education

Summary #

Let’s summarize what we have learned in this lesson.

This lesson mainly introduces two typical use cases of ThreadLocal.

Use case 1: ThreadLocal is used to store objects that are unique to each thread. An individual copy is created for each thread, and each thread can only modify its own copy without affecting other threads’ copies. This makes the originally thread-unsafe situation thread-safe in concurrent scenarios.

Use case 2: ThreadLocal is used to store information that needs to be independently saved within each thread and is conveniently accessed by other methods. Each thread can retrieve different information, and the preceding methods can set the information, while the subsequent methods can directly retrieve it through ThreadLocal, avoiding the need for parameter passing.