Best practice ThreadLocal in Java
As soon as you start to work on a multiuser environment on backend (server) or on a frontend you will need threads. Sadly now you cannot use anymore static variables to store your data globally for a user. But there is a way there you can attach some global data to a specific thread. ThreadLocal is a commonly known pattern for this case in many languages, also in Java.
Seldomly you will find details about common pitfalls of ThreadLocal in Java. Go on reading to understand the ThreadLocal pattern and what you should avoid.
Table of Contents
this article is work in progress
1. Common Pitfalalls
Developers who are new to ThreadLocal in Java are pretty happy to have a solution to access a global variable with specific data for this user. Often they forget, that the threads are getting provided from a thread pool. Means the threads are created on startup or to be more specific on the initialization of the thread pool itself. So you access either by a worker or by the threadpool wrapper a thread, which has an undefined lifetime. Let me give you an example. You have a server application with about 10.000 concurrent users over the day. The server itself runs usually without any restart over weeks or months. To reduce the load of creating new threads all common java servers have thread pools, which get initialized on startup and keep threads prepared for the workload.
1.1. Cleanup your ThreadLocal on leaving the thread
Means the thread which you get in such an environment may be already weeks old! So if you do not consider the corner cases in such environment your application may fail regularly. Let me explain one real life example I had to fix in the past. I was called for urgent support in a high workload and high revenue (tens of millions EUR a day) application. This application was terminating regularly without any clue by the developers what happend.
After analyzing the code and the production runtime I found a issue with the ThreadLocal usage. The developers created a ThreadLocal store to measure the network performance of a specific user. So they added the performance values to the ThreadLocal. On the response they passed these details to the user to visualize the performance. This was working great, but...
What the developers forgot is to cleanup the ThreadLocal after giving the Thread back to the thread pool. Means every time the thread was picked from the thread pool, the ThreadLocal store was extended with more performance data. Means userA added his performance data to the ThreadLocal, userX added his performance data to the ThreadLocal, and so on. So the ThreadLocal was growing other the time until the heap/memory was totally eaten.
1.2. Use only one ThreadLocal for all your thread global data
Often in code reviews I see multiple ThreadLocal variables also in teams with a highly skilled developers. So what is the issue with multiple ThreadLocal variables you may ask?
Let assume you have independent teams, the one is doing some technical stuff, the other some business areaA the other the business areaB and so on. So if everyone is storing data into the ThreadLocal you quickly loose the overview what thread global data is getting persisted and also in the worst case have to be synchronized over multiple nodes in clustered environments.
To avoid this use a ThreadLocal Context class, which keeps all references you need in your application. This is already shown above how to use a ThreadLocalContext.
1.3. ThreadLocal data needs to live longer than the usage time of the given thread
This is one of the common things which may happen. You have data, which of course is needed during the usage time of your thread. But sadly this data has to live even longer. Now you may have multiple options to keep this data. Keep the data in
- in a persistent storage (e.g. you database)
- in the application scope (time from starting the application till ending it)
- in the session of the user (only valid from login until the session gets terminated)
- in the request scope (time from request entry point to its response)
- in the Thread scope (time from start using the thread till end using the thread)
- in the class scope
- in the method scope
The longer you need to keep the data, the higher the impact is on potential high availability and high performance requirements.
if you use data variables in the method scope the lifetime of the variable will be pretty short. So as long as you do not need to share this data along multiple threads or server-instances, you have no impact on the scalability and thus performance on your application. Nearly the same occurs on the lifetime of the class scope. As long as your class needs not to be synchronized/share across multiple threads or server instances, you do not have a big issue on scalability. Of course you need to keep an eye on synchronizing method calls, but this is some basic multithreading knowledge, you have to have if you develop on multithreaded application (e.g. with webservers).
The impact start the higher you go up in the above given list. The ThreadLocal e.g. is already living longer als all of your created class instances. If you work on the request scope, you may already use multiple threads. If you go into the session scope you have already defined to use multiple requests, which itself have used already multiple threads. And so on.
Now if you need some data in a session of the user, the pain in clustered environment begins. Here you have to ensure, to have consistent session data over all nodes and over all nodes. Even if a handover from nodeA to nodeB was done, your session needs to be kept consistent. This consistency requires a high level of synchronization of your sessions over all nodes. And if you go to the application scope, here the data also has to live longer as multiple sessions. And on the top level, you have data which have to be kept over mutlitple application restarts.
2. Conclusion
Global variables are always a issue for keeping a application as much stateless as possible. But sometimes real life need some userspecific context data which itself may have a lifetime over multiple threads.