← Back to Multithreading Course

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:

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.

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.

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:

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.

Congratulations! You Have Completed Part 6

We have explored several useful ways to manage shared data and protect resources in multithreaded applications.

We learned about ReentrantLock, ReadWriteLock, thread-safe collections, ThreadLocal, and immutable objects.

Now we are ready to move towards a more modern style of programming.

Next: Part 7 — Asynchronous Programming and Modern Java

So far, we have mostly worked with threads and tasks directly or through the Executor Framework.

In the next part, we will explore CompletableFuture, which makes it easier to run asynchronous tasks, combine their results, and handle failures.

We will also learn about parallel streams and virtual threads.

One step at a time. See you in Class 38!

0% Completed

You have completed 37 out of 46 classes.