Skip to main content
Java

How to use Java ThreadLocal with Thread Pools ‐ A Practical Use Case

9 mins

A row of lockers, each labeled for a specific person, symbolizing how ThreadLocal provides thread-specific storage

Passing Data in Multi-threaded Applications #

In a multithreaded application, you may have a set of data that is only specific to a particular thread. For example, you might have user-specific data that needs to be accessed by different threads without interference from one another.

One common approach is to create a data structure, such as a Map or a custom data object, to hold the data for a specific user. You then have to pass this data around in method parameters. This can be cumbersome as this object needs to be passed through every method that requires access to it, leading to complex method signatures and potential errors if the data is not passed correctly.

For example, consider a scenario where you code needs access to user information in various methods:

public class UserManager {
    public void addTask(Map<String, String> userDataMap, String task) {
        // utility method to check if task is valid for user
        if (isValidTask(userDataMap, task)) {

            taskService.addTask(userDataMap, task); 

            reminderService.send(userDataMap, task);
        } else {
            throw new IllegalArgumentException("Invalid task for user");
        }
    }
}

This becomes even more complicated when methods there are a hierarchy of methods that need access to the user data, leading to a proliferation of parameter passing.

ThreadLocal: A Cleaner Solution #

ThreadLocal provides a cleaner solution to this problem by allowing each thread to maintain its own independent copy of a variable. This means that you can store data in a ThreadLocal variable, and each thread will have its own instance of that data without needing to pass it around explicitly.

A thread-local is created using the ThreadLocal class in Java. You define what type of data you want to store in the thread-local variable, and then you can set and get values from it.

Consider a simple example where an integer value is stored in a ThreadLocal variable:

public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);    

    public static void main(String[] args) {
        // Create a thread that modifies the thread-local value
        Thread thread1 = new Thread(() -> {
            threadLocalValue.set(10);
            System.out.println("Thread 1: " + threadLocalValue.get());
        });

        // Create another thread that modifies the thread-local value
        Thread thread2 = new Thread(() -> {
            threadLocalValue.set(20);
            System.out.println("Thread 2: " + threadLocalValue.get());
        });

        // Start both threads
        thread1.start();
        thread2.start();

        // Wait for both threads to finish
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Print the value in the main thread
        System.out.println("Main Thread: " + threadLocalValue.get());
    }

}

This will output:

Thread 1: 10
Thread 2: 20
Main Thread: 0

So even though both threads are accessing the same ThreadLocal variable, they each have their own independent copy of the value. The main thread has its own copy as well, which is initialized to 0.

Be Careful with Thread Pools #

When using a thread pool, you need to be careful with ThreadLocal variables. The threads in a thread pool are reused, which means that the data stored in a ThreadLocal variable can persist across different tasks executed by the same thread.

Reset the ThreadLocal variable at the beginning of each task.

You should clear the ThreadLocal variable after the task is completed or reset it at the beginning of each task, so that data from a previous task does not leak into the next task.

Use Case: User Data Access #

In a typical web application, you might need to store user-specific data (e.g., user ID, preferences) that is accessed by various components throughout the request lifecycle. Using ThreadLocal, you can store this data in a way that is isolated to the current thread handling the request.

Since most applications require multiple variables that are specific to a thread, creating multiple ThreadLocal variables can be cumbersome. Instead, you can create a single ThreadLocal variable that holds a map or a custom data object containing all the thread-specific data.

Creating a ThreadLocal Variable for User Data #

Let’s start by creating a ThreadLocal variable that holds a map of user-specific data. All methods are statically accessible, so we can easily set and get user data without passing it around.

package com.programmerpulse.threadlocalex;

import java.util.HashMap;
import java.util.Map;

public class UserData {
    private static final ThreadLocal<Map<String, String>> THREAD_LOCAL_DATA_MAP = ThreadLocal.withInitial(() -> {
        Map<String, String> map = new HashMap<>();
        return map;
    });
    
    public static void setValue(String key, String value) {
        Map<String, String> map = THREAD_LOCAL_DATA_MAP.get();
        map.put(key, value);
    }

    public static String getValue(String key) {
        Map<String, String> map = THREAD_LOCAL_DATA_MAP.get();
        return map.get(key);
    }

    public static void printMap() {
        Map<String, String> map = THREAD_LOCAL_DATA_MAP.get();
        System.out.printf("(%s) ThreadLocal Data Map: %s%n", Thread.currentThread().getName(), map);
    }

    public static void clearData() {
        THREAD_LOCAL_DATA_MAP.remove();
    }
}

The THREAD_LOCAL_DATA_MAP variable is a ThreadLocal variable that holds a map of user-specific data. This means that each thread has its own independent copy of the map, and changes made by one thread do not affect the maps of other threads.

Using the ThreadLocal Variable #

Next we can create a simple task that uses UserData to set and retrieve user-specific data. This task will simulate a scenario where a thread sets some user data, processes it, and then retrieves the user profile without passing the data around.

package com.programmerpulse.threadlocalex;

public class RunnableTask implements Runnable {

    @Override
    public void run() {
        // Clear any existing data in the ThreadLocal storage
        UserData.clearData();

        // Generate random data for demonstration purposes
        String rndId = String.valueOf((int) (Math.random() * 100000));
        String randomKey = "key" + (int) (Math.random() * 100000);
        String randomValue = "value" + (int) (Math.random() * 100000);

        System.out.printf("(%s) Setting data: id = %s, %s = %s%n",
                Thread.currentThread().getName(), rndId, randomKey, randomValue);

        // Set new data in the ThreadLocal storage
        UserData.setValue("id", rndId);
        UserData.setValue(randomKey, randomValue);

        // Simulate some processing
        try {
            Thread.sleep(500); // Simulate work by sleeping for 500 milliseconds
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // Restore interrupted status
        }

        // Print the ThreadLocal data map
        UserData.printMap();

        // Fetch user profile using the UserManager class
        UserManager userManager = new UserManager();
        userManager.getUserProfile();
        System.out.printf("(%s) --- Finished processing --- %n", Thread.currentThread().getName());
    }
}

This task sets some random data in the ThreadLocal variable and then simulates some processing by sleeping for a short time. After that, it prints the contents of the ThreadLocal data map.

Accessing Data without Passing Parameters #

To illustrate that we do not need to pass the user data around, we can create a UserManager class that retrieves the user profile using the data stored in the ThreadLocal variable:

package com.programmerpulse.threadlocalex;

public class UserManager {
    public String getUserProfile() {
        String id = UserData.getValue("id");

        // Simulate fetching user profile information based on the ID
        System.out.printf("(%s) fetch user profile for id = %s%n", Thread.currentThread().getName(), id);
        return "User Profile Info";
    }
}

Running in a Thread Pool #

To run this in a thread pool, we can use the ExecutorService. This would simulate multiple threads used in a web application or any multithreaded environment where tasks are executed concurrently.

package com.programmerpulse.threadlocalex;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class RunService {
    private ExecutorService executorService = Executors.newFixedThreadPool(2);


    public void startService() {
        // This method is intended to start a service.
        // The implementation details would depend on the specific service being started.
        System.out.println("Service started.");

        RunnableTask myRunnable = new RunnableTask();
        for (int i = 0; i < 3; i++) {
            executorService.submit(myRunnable);
        }
    }

    public static void main(String[] args) {
        RunService runService = new RunService();
        runService.startService();

        // Shutdown the executor service after use
        runService.executorService.shutdown();
    }

}

We deliberately created a thread pool with only two threads to run three tasks. This is to show how the ThreadLocal variable behaves when tasks are executed by the same thread in the pool.

Run the Service #

When running the RunService class, you will see output similar to the following:


Service started.
(pool-1-thread-2) Setting  data: id = 6342, key58165 = value37755
(pool-1-thread-1) Setting  data: id = 47009, key42622 = value63923
(pool-1-thread-2) ThreadLocal Data Map: {id=6342, key58165=value37755}
(pool-1-thread-1) ThreadLocal Data Map: {key42622=value63923, id=47009}
(pool-1-thread-2) fetch user profile for id = 6342
(pool-1-thread-2) --- Finished processing --- 
(pool-1-thread-1) fetch user profile for id = 47009
(pool-1-thread-1) --- Finished processing --- 
(pool-1-thread-2) Setting  data: id = 15507, key57729 = value54634
(pool-1-thread-2) ThreadLocal Data Map: {id=15507, key57729=value54634}
(pool-1-thread-2) fetch user profile for id = 15507
(pool-1-thread-2) --- Finished processing --- 

A few things to note:

  • There are only two threads in the pool, indicated by the thread names pool-1-thread-1 and pool-1-thread-2.

  • Each thread has its own independent ThreadLocal data map. The output shows that the values set by each thread are isolated. For example, pool-1-thread-2 has its own data map with id=6342 and key58165=value37755, while pool-1-thread-1 has id=47009 and key42622=value63923.

  • The third task is executed by pool-1-thread-2, which reuses the thread from the pool. It sets new data in the ThreadLocal variable, demonstrating that the previous data does not leak into the next task.

You see the interactions between the UserData helper class and the ThreadLocal variable being used by the threads in the following diagram:

flowchart LR subgraph Thread-1 A1[ThreadLocal Map
id=6342
key58165=value37755] end subgraph Thread-2 B1[ThreadLocal Map
id=47009
key42622=value63923] end subgraph Thread-1_Reused A2[ThreadLocal Map
id=15507
key57729=value54634] end UserData1((UserData.setValue)) UserData2((UserData.setValue)) UserData3((UserData.setValue)) UserData1 --> A1 UserData2 --> B1 UserData3 --> A2 A1 -. reused .-> A2 style Thread-1 fill:#e0f7fa,stroke:#26c6da style Thread-2 fill:#f1f8e9,stroke:#8bc34a style Thread-1_Reused fill:#e0f7fa,stroke:#26c6da

Not clearing ThreadLocal Data #

Let’s see what happens if we do not clear the ThreadLocal data after each task. If we run the same code without calling ThreadData.clearData() at the beginning of the run() method in MyDataRunnable.

Comment out the line UseData.clearData(); in the run() method:

    @Override
    public void run() {
        // Clear any existing data in the ThreadLocal storage
        // UseData.clearData();

        // ... rest of the code remains the same
    }

The output will now look like this:


Service started.
(pool-1-thread-1) Setting  data: id = 54113, key5694 = value34008
(pool-1-thread-2) Setting  data: id = 99194, key63868 = value48030
(pool-1-thread-2) ThreadLocal Data Map: {key63868=value48030, id=99194}
(pool-1-thread-1) ThreadLocal Data Map: {id=54113, key5694=value34008}
(pool-1-thread-1) fetch user profile for id = 54113
(pool-1-thread-1) --- Finished processing --- 
(pool-1-thread-2) fetch user profile for id = 99194
(pool-1-thread-2) --- Finished processing --- 
(pool-1-thread-1) Setting  data: id = 74469, key31744 = value32718
(pool-1-thread-1) ThreadLocal Data Map: {key31744=value32718, id=74469, key5694=value34008}
(pool-1-thread-1) fetch user profile for id = 74469
(pool-1-thread-1) --- Finished processing --- 

You can see that the ThreadLocal data map for pool-1-thread-1 now contains data from the previous task, which is not what we want.

e.g. it has key5694=value34008 from the first task, along with the new data key31744=value32718 and id=74469.

Memory Leak Risk

If the data was not cleared, the map would continue to grow with each task executed by the same thread. This could lead to out of memory errors or unintended data sharing between tasks, especially in a long-running application where threads are reused frequently such as a long running web server.

Conclusion #

Using ThreadLocal in Java provides a clean and efficient way to manage thread-specific data in multithreaded applications. It allows each thread to maintain its own independent copy of a variable, eliminating the need for complex synchronization mechanisms.

However, it’s crucial to manage the lifecycle of ThreadLocal variables properly. Failing to clear ThreadLocal data can lead to memory leaks and unintended data sharing between tasks, especially when reusing threads such as in thread pools. Always ensure to clear or reset ThreadLocal variables at the beginning or end of each task.