Skip to content
On this page

ThreadLocal

Multithreading is the foundation for implementing multiple tasks in Java. The Thread object represents a thread, and we can call Thread.currentThread() in the code to get the current thread. For example, when printing logs, we can simultaneously print the name of the current thread:

java
// Thread
public class Main {
    public static void main(String[] args) throws Exception {
        log("start main...");
        new Thread(() -> {
            log("run task...");
        }).start();
        new Thread(() -> {
            log("print...");
        }).start();
        log("end main.");
    }

    static void log(String s) {
        System.out.println(Thread.currentThread().getName() + ": " + s);
    }
}

For multiple tasks, the Java standard library provides thread pools to conveniently execute these tasks while reusing threads. Web applications are typical examples of multitasking applications. Each user request to a page creates a task, similar to:

java
public void process(User user) {
    checkPermission();
    doWork();
    saveStatus();
    sendResponse();
}

Then, these tasks are executed through a thread pool.

Observing the process() method, it internally calls several other methods. We encounter a problem: how to pass the state within a thread?

The state that the process() method needs to pass is the User instance. Some might think that simply passing User is sufficient:

java
public void process(User user) {
    checkPermission(user);
    doWork(user);
    saveStatus(user);
    sendResponse(user);
}

However, often a method will call many other methods, leading to the User being passed everywhere:

java
void doWork(User user) {
    queryStatus(user);
    checkStatus();
    setNewStatus(user);
    log();
}

This kind of object that needs to be passed across several method calls within a thread is commonly referred to as a context. The context is a state that can include user identity, task information, etc.

Adding a context parameter to every method is cumbersome. Moreover, sometimes, if the call chain involves third-party libraries with unmodifiable source code, the User object cannot be passed in.

The Java standard library provides a special ThreadLocal that allows passing the same object within a thread.

A ThreadLocal instance is usually initialized as a static field as follows:

java
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

Its typical usage is as follows:

java
void processUser(User user) {
    try {
        threadLocalUser.set(user);
        step1();
        step2();
        log();
    } finally {
        threadLocalUser.remove();
    }
}

By associating a User instance with the ThreadLocal, all methods can access the User instance at any time before it is removed:

java
void step1() {
    User u = threadLocalUser.get();
    log();
    printUser();
}

void step2() {
    User u = threadLocalUser.get();
    checkUser(u.id);
}

void log() {
    User u = threadLocalUser.get();
    println(u.name);
}

Notice that ordinary method calls are always executed by the same thread, so within the step1(), step2(), and log() methods, the threadLocalUser.get() retrieves the same User instance.

In fact, you can think of ThreadLocal as a global Map<Thread, Object>: when each thread accesses a ThreadLocal variable, it always uses the thread itself as the key:

java
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

Therefore, ThreadLocal effectively provides each thread with its own separate storage space, and the instances associated with ThreadLocal in different threads do not interfere with each other.

Finally, it is crucial to always remove ThreadLocal in a finally block:

java
try {
    threadLocalUser.set(user);
    ...
} finally {
    threadLocalUser.remove();
}

This is because after the current thread finishes executing the relevant code, it is likely to be returned to the thread pool. If the ThreadLocal is not cleared, the thread may carry over the previous state when executing other code.

To ensure that the instances associated with ThreadLocal are released, we can use the AutoCloseable interface in conjunction with the try (resource) { ... } structure, allowing the compiler to automatically close the resource for us. For example, a ThreadLocal that stores the current username can be encapsulated as a UserContext object:

java
public class UserContext implements AutoCloseable {

    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public UserContext(String user) {
        ctx.set(user);
    }

    public static String currentUser() {
        return ctx.get();
    }

    @Override
    public void close() {
        ctx.remove();
    }
}

When using it, we can write:

java
try (var ctx = new UserContext("Bob")) {
    // Can call UserContext.currentUser() at any time:
    String currentUser = UserContext.currentUser();
} // Automatically calls UserContext.close() to release the ThreadLocal-associated object

This completely encapsulates the ThreadLocal within UserContext. External code can call UserContext.currentUser() at any time within the try (resource) { ... } block to obtain the username bound to the current thread.

Exercise

Practice using ThreadLocal.

Summary

  • ThreadLocal represents a thread's "local variable," ensuring that each thread's ThreadLocal variable is independent.
  • ThreadLocal is suitable for maintaining context within a thread's processing flow (avoiding the need to pass the same parameter through all methods).
  • When using ThreadLocal, always use a try ... finally structure and remove it in the finally block.
ThreadLocal has loaded