1. Introduction
In this article, I will introduce ThreadLocal in Java. It has the ability to store data individually for the current stream – and just encapsulate it in a special type of object.
2. ThreadLocal API
TheadLocal structure allows us to store data that can only be accessed by a particular thread.
Assume that we want an Integer that will be wrapped in a specific Thread like so:
1 2 | ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>(); |
Next, when we want to use this value from a Thread, we just need to call a get()
or set()
. Simply put, we can think that ThreadLocal stores data inside the map – with the thread being the key.
Therefore, when we call a get()
on threadLocalValue
, we get an Integer value for the requesting thread:
1 2 3 | threadLocalValue.set(1); Integer result = threadLocalValue.get(); |
We can create an instance of ThreadLocal using the static method withInitial()
and pass the respective supplier:
1 2 | ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1); |
To remove the value from the ThreadLocal, we can call the remove()
:
1 2 | threadLocal.remove(); |
To see how to properly use ThreadLocal, we will first look at an example that doesn’t use ThreadLocal, then we’ll rewrite our example to take advantage of that structure.
3. Store User Data in a Map
Consider a program that needs to store data – user-specific Context
per user id:
1 2 3 4 5 6 7 8 | public class Context { private String userName; public Context(String userName) { this.userName = userName; } } |
We want one thread per userId. We will create a class SharedMapWithUserContext
implements the Runnable
interface. The implementation in the run()
calls some database through the UserRepository
class, UserRepository
returns a Context
object for a certain userId
.
Next, we store that Context
in the ConcurentHashMap
map with the key is userId
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class SharedMapWithUserContext implements Runnable { public static Map<Integer, Context> userContextPerUserId = new ConcurrentHashMap<>(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContextPerUserId.put(userId, new Context(userName)); } // standard constructor } |
We can easily test our code by creating and starting 2 threads for 2 different userId and confirming that we have two entries in the userContextPerUserId
map:
1 2 3 4 5 6 7 | SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1); SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start(); assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2); |
4. Store the User Data in ThreadLocal
We can rewrite the above example to store the user’s Context
using ThreadLocal
. Each thread will have its own ThreadLocal
instance.
When using ThreadLocal
, we need to be very careful because every ThreadLocal
instance is associated with a specific thread. In the example, we have a thread that is dedicated to each particular userId and this thread is created by us, so we have full control over it.
The run()
will fetch the user context and store it in the ThreadLocal
variable using the set()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class ThreadLocalWithUserContext implements Runnable { private static ThreadLocal<Context> userContext = new ThreadLocal<>(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContext.set(new Context(userName)); System.out.println("thread context for given userId: " + userId + " is: " + userContext.get()); } // standard constructor } |
We can test it out by starting two threads that will execute the action for a given userId:
1 2 3 4 5 | ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1); ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start(); |
After running this code, we will see on the standard output that ThreadLocal
has been set for each given thread:
1 2 3 | thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'} thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'} |
We can see that each user has its own Context
.
5. ThreadLocals and Thread Pools
ThreadLocal
provides an easy to use API that limits a number of values per thread. This is a sensible way to achieve thread safety in Java. However, we should be extra careful when using ThreadLocals and thread pool together.
To better understand the probable warning, let’s consider the following scenario:
- First, the application borrows a thread from the pool.
- Then it stores some thread limit value into the current thread’s ThreadLocal.
- When the current execution ends, the application returns the thread borrowed to the group.
- After a while, the application borrows the same thread to process another request.
- Since the application last time did not perform the necessary cleanup, it can reuse the same ThreadLocal data for the new request.
This can have surprising consequences in highly concurrent applications.
One way to get around this problem is to manually delete each ThreadLocal
after we are finished using it. Since this approach requires strict code consideration, it can be prone to errors.
5.1. Expand ThreadPoolExecutor
As it turns out, the ThreadPoolExecutor
class can be extended and provides a custom hook implementation for the beforeExecute()
and afterExecute()
. The thread pool will call the beforeExecute()
before running anything on the borrowed thread. On the other hand, it will call the afterExecute()
after executing our logic.
Therefore, we can extend the ThreadPoolExecutor class and remove the ThreadLocal data in the afterExecute () method:
1 2 3 4 5 6 7 8 | public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor { @Override protected void afterExecute(Runnable r, Throwable t) { // Call remove on each ThreadLocal } } |
If we send our request to implement this ExecutorService
, then we can be sure that using ThreadLocal
and thread pools will not pose security risks to our application.
6. Conclusion
In this article, we looked at the ThreadLocal
structure. We have implemented logic that uses ConcurrentHashMap
is shared between threads to store the context associated with a particular userId. Next, we rewrite our example to leverage ThreadLocal
to store data associated with a particular userId and to a specific thread.
You can refer to how to implement with an example project on github