Class 37: ThreadLocal and Immutable Objects
Part 6 — Locks and Concurrent Collections
In the previous class, we learned how Java provides thread-safe collections such as ConcurrentHashMap and CopyOnWriteArrayList.
We saw how multiple threads can safely work with shared data using the right collection.
But what if we do not want threads to share certain values at all?
Imagine an application where every thread needs to maintain its own user ID, request information, or temporary data.
We do not want one thread's value to interfere with another thread's value.
Java provides a feature called ThreadLocal for this situation.
We will also understand immutable objects and why they are useful in multithreaded applications.
1. Understanding ThreadLocal
Let's start with a simple example.
Imagine three employees working on three different tasks. Each employee needs to maintain a personal notebook.
Employee 1 writes information in their notebook. Employee 2 writes information in their own notebook. Employee 3 does the same.
They do not overwrite one another's information because each employee has a separate notebook.
ThreadLocal works in a similar way.
It allows each thread to maintain its own separate value, even when multiple threads use the same ThreadLocal variable.
The important point is:
- Multiple threads can access the same ThreadLocal object.
- Each thread gets its own separate value associated with that object.
- One thread's value does not overwrite another thread's value.
2. Creating a ThreadLocal Variable
Let's see how to create a ThreadLocal variable in Java.
ThreadLocal<String> threadLocal = new ThreadLocal<>();
We can store a value for the current thread using set().
threadLocal.set("Hello");
We can retrieve the current thread's value using get().
String value = threadLocal.get();
We can remove the current thread's value using remove().
threadLocal.remove();
Notice that these operations work with the value associated with the current thread.
3. A Practical Example
Let's create a ThreadLocal variable that stores a name for each thread.
public class ThreadLocalExample {
private static ThreadLocal<String> userName =
new ThreadLocal<>();
public static void main(String[] args) {
Runnable task1 = () -> {
userName.set("Ankit");
System.out.println(
Thread.currentThread().getName()
+ " : " + userName.get()
);
};
Runnable task2 = () -> {
userName.set("Rahul");
System.out.println(
Thread.currentThread().getName()
+ " : " + userName.get()
);
};
Thread thread1 = new Thread(task1, "Thread 1");
Thread thread2 = new Thread(task2, "Thread 2");
thread1.start();
thread2.start();
}
}
Both threads are accessing the same static ThreadLocal variable called userName.
However, Thread 1 stores "Ankit", while Thread 2 stores "Rahul".
Each thread reads its own value.
The output will contain:
Thread 1 : Ankit
Thread 2 : Rahul
The order of the two output lines may vary, but each thread will read the value it stored.
This is the main purpose of ThreadLocal.
4. Using initialValue()
What happens if a thread calls get() without first calling set()?
By default, a regular ThreadLocal returns null when the current thread has not set a value.
We can provide an initial value using
ThreadLocal.withInitial().
ThreadLocal<String> userName =
ThreadLocal.withInitial(() -> "Guest");
Now, if a thread calls get() without setting a value, it receives "Guest".
Let's see a complete example.
public class ThreadLocalInitialValueExample {
private static ThreadLocal<String> userName =
ThreadLocal.withInitial(() -> "Guest");
public static void main(String[] args) {
Runnable task = () -> {
System.out.println(
Thread.currentThread().getName()
+ " : " + userName.get()
);
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
thread1.start();
thread2.start();
}
}
Both threads receive their own initial value. In this example, both values are "Guest".
5. Removing ThreadLocal Values
There is one important thing to remember when using ThreadLocal.
Applications often use thread pools, where the same worker thread handles multiple tasks over time.
If a task stores a value in ThreadLocal and does not remove it, that value may remain associated with the worker thread after the task finishes.
A later task using the same worker thread could accidentally see that old value if it does not set its own value first.
Therefore, when a ThreadLocal value is no longer needed, remove it.
try {
userName.set("Ankit");
// Perform task using the thread-local value.
} finally {
userName.remove();
}
The finally block ensures that the value is removed even if an exception occurs.
This is especially important when using ThreadLocal with thread pools.
6. Where Can ThreadLocal Be Useful?
Let's consider a few practical situations.
- Request information: Keeping information associated with the current request-processing thread.
- User context: Maintaining a user identifier while a thread performs a task.
- Temporary values: Storing data that should remain separate for each thread.
ThreadLocal is not a replacement for synchronization. It solves a different problem by giving each thread its own value instead of making all threads share one value.
7. Understanding Immutable Objects
Now let's move to our second concept.
We have spent several classes learning how to protect shared data from concurrent modifications.
But what if an object cannot be modified after it has been created?
That is the basic idea behind an immutable object.
An immutable object is an object whose state cannot be changed after it has been created.
Think about a printed photograph.
Once the photograph is printed, you cannot change the original image simply by looking at it or sharing it. You would need to create another photograph to represent a different image.
Immutable objects work in a similar way.
8. Creating an Immutable Class
Let's create a simple Employee class.
public final class Employee {
private final int id;
private final String name;
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
Let's understand what makes this class immutable.
- The class is declared final, so it cannot be extended.
- The fields are private, so they cannot be accessed directly from outside.
- The fields are final, so they can be assigned only once.
- There are no setter methods to change the values.
Once an Employee object is created, its ID and name cannot be changed through this class.
Employee employee = new Employee(101, "Ankit");
System.out.println(employee.getId());
System.out.println(employee.getName());
We can read the employee details, but we cannot change them after creation.
9. Why Are Immutable Objects Useful in Multithreading?
Let's connect this concept with multithreading.
Imagine that five threads are accessing the same Employee object.
If the object is immutable, none of those threads can change its state after it has been created.
Therefore, we do not need to worry about multiple threads changing the employee's ID or name at the same time.
This makes immutable objects much easier to share between threads safely.
We are avoiding the problem instead of trying to control every modification with locks.
10. An Important Point: Mutable Objects Inside Immutable Objects
There is one more thing we should understand.
Making fields private and final is not always enough to make an object truly immutable.
Imagine that our Employee class contains a mutable Date object.
public final class Employee {
private final java.util.Date joiningDate;
public Employee(java.util.Date joiningDate) {
this.joiningDate = joiningDate;
}
public java.util.Date getJoiningDate() {
return joiningDate;
}
}
The reference joiningDate is final, but the Date object itself can still be modified.
Someone could change the date through the original reference or through the getter.
This means the Employee object is not truly immutable.
We need to protect mutable objects by making defensive copies when accepting them and when returning them.
public final class Employee {
private final java.util.Date joiningDate;
public Employee(java.util.Date joiningDate) {
this.joiningDate =
new java.util.Date(joiningDate.getTime());
}
public java.util.Date getJoiningDate() {
return new java.util.Date(joiningDate.getTime());
}
}
Now the class keeps its own copy of the date and returns a new copy whenever someone requests it.
This prevents outside code from changing the internal Date object.
The same idea applies to other mutable objects such as lists and maps.
11. ThreadLocal vs. Immutable Objects
Both concepts are useful in multithreaded applications, but they solve different problems.
| ThreadLocal | Immutable Objects |
|---|---|
| Each thread maintains its own value. | An object's state cannot change after creation. |
| Helps avoid sharing certain values between threads. | Makes shared objects safer to use. |
| Useful for thread-specific data. | Useful for data that should remain unchanged. |
Remember these two simple ideas:
- ThreadLocal: Give each thread its own value.
- Immutable objects: Share data that cannot be changed.
Test Your Understanding
Let's see how well you understood this class!
Question 1: What is the main purpose of ThreadLocal?
Question 2: Which method stores a value in ThreadLocal for the current thread?
Question 3: Why should we call remove() when a ThreadLocal value is no longer needed?
Question 4: What is an immutable object?
Question 5: Why are immutable objects useful in multithreaded applications?
Class Summary
Let's quickly revise what we learned today.
- ThreadLocal allows each thread to maintain its own separate value.
- set() stores a value for the current thread.
- get() retrieves the current thread's value.
- remove() removes the current thread's stored value.
- ThreadLocal.withInitial() provides an initial value.
- Removing ThreadLocal values is important when using thread pools.
- Immutable objects cannot be changed after creation.
- Immutable objects are easier to share safely between threads.
- Mutable objects inside immutable classes need additional protection, such as defensive copying.
Remember: Sometimes the simplest way to avoid a shared-data problem is not to share the data in the first place. And when data must be shared, immutable objects can make things much safer.
You have completed 37 out of 46 classes.