ThreadLocal in Java
ThreadLocal is a Java class that allows each thread to store and access its own independent copy of a variable. The value stored in one thread is completely isolated from other threads.
ThreadLocal is mainly used when data belongs to a specific thread, such as the currently logged-in user, request information, database session, transaction context, or logging information.
Why Do We Need ThreadLocal?
In multi-threaded applications, multiple threads execute code simultaneously. Sharing variables between threads can create data inconsistency and synchronization problems.
- Store thread-specific data
- Avoid passing context objects through multiple method calls
- Provide thread isolation
- Reduce synchronization requirements
- Used internally by frameworks like Spring Security and Hibernate
Problem Without ThreadLocal
Consider a shared variable storing the current user. If multiple threads update the same variable, one thread can overwrite another thread's data.
class UserContext {
static String currentUser;
}
public class Main {
public static void main(String[] args) {
Runnable task1 = () -> {
UserContext.currentUser = "Rahul";
System.out.println(UserContext.currentUser);
};
Runnable task2 = () -> {
UserContext.currentUser = "Amit";
System.out.println(UserContext.currentUser);
};
new Thread(task1).start();
new Thread(task2).start();
}
}Since both threads access the same variable, the output can be unpredictable. One thread can modify the value while another thread is using it.
ThreadLocal Basic Example
public class UserContext {
private static final ThreadLocal<String> currentUser = new ThreadLocal<>();
public static void setUser(String user){
currentUser.set(user);
}
public static String getUser(){
return currentUser.get();
}
public static void clear(){
currentUser.remove();
}
}Using ThreadLocal With Multiple Threads
public class Main {
public static void main(String[] args) {
Runnable task1 = () -> {
UserContext.setUser("Rahul");
System.out.println(
Thread.currentThread().getName()
+ " -> "
+ UserContext.getUser()
);
UserContext.clear();
};
Runnable task2 = () -> {
UserContext.setUser("Amit");
System.out.println(
Thread.currentThread().getName()
+ " -> "
+ UserContext.getUser()
);
UserContext.clear();
};
new Thread(task1).start();
new Thread(task2).start();
}
}Output
Thread-0 -> Rahul
Thread-1 -> AmitEach thread receives its own copy of the value. Changing the value in one thread does not affect another thread.
How ThreadLocal Works Internally
Many developers think ThreadLocal stores values internally. Actually, every Thread object maintains its own ThreadLocalMap. The ThreadLocal key points to a value stored inside the current thread.
Thread-1
--------------------
ThreadLocalMap
--------------------
ThreadLocal<String> -> Rahul
ThreadLocal<User> -> User Object
--------------------
Thread-2
--------------------
ThreadLocalMap
--------------------
ThreadLocal<String> -> Amit
ThreadLocal<User> -> User Object
--------------------ThreadLocal Object Storage
ThreadLocal can store any Java object. It is not limited to primitive values or strings.
- String
- Integer
- Custom Java objects
- DTO objects
- Lists
- Maps
- Request context objects
ThreadLocal With Custom Object
class User {
private String name;
private String role;
public User(String name,String role){
this.name=name;
this.role=role;
}
public String getName(){
return name;
}
public String getRole(){
return role;
}
}
ThreadLocal<User> userHolder = new ThreadLocal<>();
userHolder.set(new User("Rahul","ADMIN"));
System.out.println(userHolder.get().getName());ThreadLocal With List
ThreadLocal can store collections also. Each thread will get its own list instance.
ThreadLocal<List<String>> rolesHolder =
ThreadLocal.withInitial(ArrayList::new);
rolesHolder.get().add("ADMIN");
rolesHolder.get().add("HOTEL_MANAGER");
System.out.println(rolesHolder.get());Output
[ADMIN, HOTEL_MANAGER]ThreadLocal With Map
ThreadLocal<Map<String,Object>> context =
new ThreadLocal<>();
Map<String,Object> data = new HashMap<>();
data.put("userId",1L);
data.put("city","Mumbai");
context.set(data);
System.out.println(context.get());ThreadLocal With Complex RequestContext Object
In real-world applications, storing a single value is not enough. Applications usually need multiple request-related details such as user information, roles, tenant information, request ID, and permissions. A custom context object can store all these values together.
public class RequestContext {
private Long userId;
private String username;
private String tenantId;
private List<String> roles;
public RequestContext(
Long userId,
String username,
String tenantId,
List<String> roles) {
this.userId = userId;
this.username = username;
this.tenantId = tenantId;
this.roles = roles;
}
public Long getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public List<String> getRoles() {
return roles;
}
}Creating ThreadLocal Holder For RequestContext
public class RequestContextHolder {
private static final ThreadLocal<RequestContext> context =
new ThreadLocal<>();
public static void set(RequestContext requestContext){
context.set(requestContext);
}
public static RequestContext get(){
return context.get();
}
public static void clear(){
context.remove();
}
}Using RequestContext
RequestContext requestContext = new RequestContext(
10L,
"Rahul",
"hotel-company-1",
List.of("HOTEL_MANAGER")
);
RequestContextHolder.set(requestContext);
System.out.println(
RequestContextHolder.get().getUsername()
);Output
RahulImportant Concept: ThreadLocal Stores Reference, Not Object Copy
ThreadLocal creates isolation between stored references. It does not create a deep copy of the object. If two threads store the same object reference, both threads will access the same object.
Example: Same Object Shared Between Threads
class User {
String name;
List<String> roles = new ArrayList<>();
User(String name){
this.name = name;
}
}
User user = new User("Rahul");
user.roles.add("USER");
ThreadLocal<User> holder = new ThreadLocal<>();
Thread t1 = new Thread(() -> {
holder.set(user);
holder.get().roles.add("ADMIN");
System.out.println(holder.get().roles);
});
Thread t2 = new Thread(() -> {
holder.set(user);
System.out.println(holder.get().roles);
});Output
Thread-1 : [USER, ADMIN]
Thread-2 : [USER, ADMIN]Both threads see ADMIN because both ThreadLocal variables contain a reference to the same User object.
ThreadLocal In Spring Boot Applications
Spring Boot applications handle thousands of HTTP requests using multiple threads. Each request is processed by a separate thread from the server thread pool. ThreadLocal allows storing request-specific data for that thread.
HTTP Request
|
v
Tomcat Worker Thread
|
v
Authentication Filter
|
v
ThreadLocal Context
|
v
Controller
|
v
Service Layer
|
v
RepositorySpring Security SecurityContextHolder Example
Spring Security uses ThreadLocal internally to store the currently authenticated user. This allows any part of the application to access the logged-in user.
Authentication authentication =
SecurityContextHolder
.getContext()
.getAuthentication();
User user =
(User) authentication.getPrincipal();
System.out.println(user.getEmail());How SecurityContextHolder Works Internally
Incoming Request
|
v
JWT Filter
|
v
Validate Token
|
v
Create Authentication Object
|
v
SecurityContextHolder
|
v
ThreadLocal<SecurityContext>
|
v
Controller / ServiceReal-World Airbnb Application Example
In a hotel booking application, when a hotel owner creates a room, the service layer needs to know the logged-in owner. Instead of passing the user object through every method, Spring Security provides it using ThreadLocal.
public RoomDto createNewRoom(Long hotelId, RoomDto roomDto){
User user = (User)
SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
Hotel hotel = hotelRepository
.findById(hotelId)
.orElseThrow();
if(!user.equals(hotel.getOwner())){
throw new RuntimeException("Unauthorized");
}
Room room = modelMapper.map(roomDto, Room.class);
room.setHotel(hotel);
return modelMapper.map(
roomRepository.save(room),
RoomDto.class
);
}Why We Do Not Pass User Everywhere
- Cleaner method signatures
- Avoid unnecessary object passing
- Framework manages lifecycle
- Every request gets its own security context
- Works naturally with HTTP request threads
ThreadLocal.withInitial()
ThreadLocal.withInitial() creates a ThreadLocal variable with a default value supplier. It avoids checking for null values manually.
ThreadLocal<List<String>> permissions =
ThreadLocal.withInitial(ArrayList::new);
permissions.get().add("READ");
permissions.get().add("WRITE");
System.out.println(permissions.get());Output
[READ, WRITE]Why ThreadLocal.remove() Is Important
In server applications like Spring Boot, threads are reused from a thread pool. If ThreadLocal values are not removed after completing a request, old data can remain attached to the thread and may be accessed by another request.
try {
RequestContextHolder.set(context);
// Business logic
processRequest();
} finally {
RequestContextHolder.clear();
}ThreadLocal Memory Leak Example
Suppose a web server has 100 threads. Each thread stores a large object in ThreadLocal but never removes it. Since threads remain alive in the pool, those objects may stay in memory longer than required.
Request 1
|
v
Thread-5
|
v
ThreadLocal -> User Object
Request completed
Thread reused for Request 2
ThreadLocal still contains old User ObjectCorrect Pattern
public void process(){
try {
threadLocal.set(object);
// execute logic
} finally {
threadLocal.remove();
}
}ThreadLocal With ThreadPool Example
ThreadLocal becomes more important when using ExecutorService because threads are reused instead of destroyed after every task.
ExecutorService executor =
Executors.newFixedThreadPool(2);
ThreadLocal<String> user =
new ThreadLocal<>();
executor.submit(() -> {
user.set("Rahul");
System.out.println(user.get());
});The same worker thread may execute another task later. Without remove(), the next task may accidentally see previous data.
ExecutorService Safe Usage
executor.submit(() -> {
try {
user.set("Rahul");
// task execution
} finally {
user.remove();
}
});InheritableThreadLocal
InheritableThreadLocal is a special type of ThreadLocal where a child thread can inherit the value from its parent thread.
public class Demo {
static InheritableThreadLocal<String> user =
new InheritableThreadLocal<>();
public static void main(String[] args){
user.set("Parent Thread User");
Thread child = new Thread(() -> {
System.out.println(user.get());
});
child.start();
}
}Output
Parent Thread UserThe child thread receives the value from the parent thread automatically.
ThreadLocal and Asynchronous Execution
Normal ThreadLocal values are not automatically transferred to another thread. This becomes important when using CompletableFuture, @Async, or custom thread pools.
ThreadLocal<String> context = new ThreadLocal<>();
context.set("User-123");
CompletableFuture.runAsync(() -> {
System.out.println(context.get());
});Output
nullThe async thread is different from the original request thread, so it does not have access to the original ThreadLocal value.
ThreadLocal in Hibernate
Hibernate and Spring transaction management use thread-bound context internally. The current database session and transaction information are associated with the executing thread.
HTTP Request
|
v
Controller Thread
|
v
@Transactional Method
|
v
Transaction Context
|
v
Hibernate Session
|
v
Database ConnectionMDC Logging With ThreadLocal
Logging frameworks like SLF4J MDC use ThreadLocal internally to store request-specific logging information such as request ID or correlation ID.
MDC.put("requestId", "REQ-101");
logger.info("Processing payment");
MDC.remove("requestId");Example Log Output
REQ-101 Processing paymentMulti-Tenant Application Example
In SaaS applications, every request belongs to a tenant. ThreadLocal can store the current tenant ID so database queries automatically use the correct tenant context.
public class TenantContext {
private static final ThreadLocal<String> tenant =
new ThreadLocal<>();
public static void setTenant(String id){
tenant.set(id);
}
public static String getTenant(){
return tenant.get();
}
public static void clear(){
tenant.remove();
}
}Common ThreadLocal Mistakes
- Forgetting to call remove()
- Using ThreadLocal as a global variable replacement
- Storing large objects unnecessarily
- Expecting values to transfer automatically to async threads
- Sharing mutable objects between threads
- Using ThreadLocal instead of dependency injection
ThreadLocal Best Practices
- Always clean ThreadLocal values in finally blocks
- Keep stored objects small
- Use ThreadLocal only for thread-specific data
- Avoid static mutable objects inside ThreadLocal
- Document ThreadLocal usage clearly
- Prefer framework-managed context when available
ThreadLocal vs Static Variable
A static variable is shared by all threads, while ThreadLocal provides an independent value for every thread.
- Static variable: One value shared by all threads
- ThreadLocal: Different value for each thread
- Static variables require synchronization in multi-threaded scenarios
- ThreadLocal avoids sharing problems
// Static variable
static String user;
// ThreadLocal variable
ThreadLocal<String> userContext = new ThreadLocal<>();ThreadLocal vs synchronized
synchronized solves shared data access problems by allowing only one thread at a time. ThreadLocal solves the problem by avoiding data sharing completely.
- synchronized protects shared resources
- ThreadLocal creates isolated resources
- synchronized can reduce performance due to locking
- ThreadLocal avoids thread contention
ThreadLocal vs Request Scope
In Spring applications, request scope and ThreadLocal both store request-specific data, but they work differently.
- Request scope is managed by Spring container
- ThreadLocal is managed by the thread itself
- Request scope follows HTTP lifecycle
- ThreadLocal follows thread lifecycle
Complete Spring Boot Request Flow Example
User Request
|
v
JWT Authentication Filter
|
v
Extract User Details
|
v
Create Authentication Object
|
v
SecurityContextHolder
|
v
ThreadLocal Storage
|
v
Controller
|
v
Service
|
v
RepositorySpring Security Internal Implementation
Spring Security uses SecurityContextHolder to store authentication information. By default, it uses ThreadLocal strategy to keep security information isolated per request thread.
public final class SecurityContextHolder {
private static SecurityContextHolderStrategy strategy;
public static SecurityContext getContext(){
return strategy.getContext();
}
}Authentication Example in Service Layer
@Service
public class BookingService {
public void createBooking(){
Authentication auth =
SecurityContextHolder
.getContext()
.getAuthentication();
User user =
(User) auth.getPrincipal();
System.out.println(
user.getEmail()
);
}
}Real Airbnb Application Scenario
In an Airbnb-style application, when a hotel owner creates a room, the application needs the currently logged-in owner. ThreadLocal through Spring Security provides this user information without passing user details manually.
public RoomDto createNewRoom(
Long hotelId,
RoomDto roomDto){
User user = (User)
SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
Hotel hotel = hotelRepository
.findById(hotelId)
.orElseThrow();
if(!user.equals(hotel.getOwner())){
throw new RuntimeException(
"Not owner"
);
}
Room room =
modelMapper.map(roomDto, Room.class);
room.setHotel(hotel);
return modelMapper.map(
roomRepository.save(room),
RoomDto.class
);
}Advantages of ThreadLocal
- Provides thread-safe storage
- Avoids unnecessary parameter passing
- Improves code readability
- Useful for request context management
- Works well with enterprise frameworks
- Reduces synchronization requirements
Disadvantages of ThreadLocal
- Can cause memory leaks if not cleaned
- Hidden dependency because data is accessed globally
- Harder to test code using ThreadLocal
- Not suitable for reactive programming without context propagation
- Can create debugging challenges
ThreadLocal Interview Questions
- What is ThreadLocal in Java?
- Why do we use ThreadLocal?
- How does ThreadLocal work internally?
- Where are ThreadLocal values stored?
- What is ThreadLocalMap?
- Can ThreadLocal store objects?
- Can ThreadLocal store List and Map?
- Does ThreadLocal create object copies?
- What happens if two threads store the same object?
- Why should we call remove()?
- What happens if ThreadLocal is not cleared?
- How does Spring Security use ThreadLocal?
- What is SecurityContextHolder?
- Difference between ThreadLocal and static variable?
- Difference between ThreadLocal and synchronized?
- Difference between ThreadLocal and InheritableThreadLocal?
- Can ThreadLocal work with ExecutorService?
- Why does ThreadLocal not work with CompletableFuture?
- How can ThreadLocal values be propagated?
- What are common ThreadLocal memory leak scenarios?
ThreadLocal Quick Cheat Sheet
Frequently Asked Questions
Can ThreadLocal store custom objects?
Yes. ThreadLocal can store any Java object including DTOs, entities, collections, maps, and custom context classes.
Does ThreadLocal make objects thread-safe?
No. ThreadLocal isolates references between threads but does not make a shared object thread-safe.
Why does Spring Security use ThreadLocal?
Because authentication information belongs to the current request thread. ThreadLocal allows controllers and services to access the logged-in user safely.
Conclusion
ThreadLocal is a powerful Java feature for managing thread-specific data. It is widely used in enterprise applications for security context, transactions, logging, and request management. However, developers must use it carefully by cleaning values after usage and avoiding unnecessary global state.