Thread-Local Variables

A thread-local variable is a variable of type ThreadLocal. Each thread that access a thread-local variable has its own, independently initialized copy of the variable. To write or read a thread-local variable's value, call its set or get method, respectively. Typically, a thread-local variable is declared as a final static field so that many components can reach it easily.

In the following example, the class TLDBConn represents a database connection. The TLBDBConn::open method prints a string and the user's name. The class TLServer represents the database itself. It contains one method, TLServer::fetchOrder, which returns a string containing the user's name. The class TLApplication creates several TLDBConn objects, each created with a different user and each in its own thread. The TLApplication::testConnection randomly varies the duration of the thread so that the threads have a chance to run concurrently.

Figure 14-6 User.java

public class User {
    
    public String name;
    
    public User(String n) {
        name = n;
    }
}

Figure 14-7 TLDBConn.java

public class TLDBConn {
    
    final static ThreadLocal<User> TLUSER = new ThreadLocal<>();
    
    public static String open(String info) {
        System.out.println(info + ": " + TLUSER.get().name);
        return info + ": " + TLUSER.get().name;
    }
}

Figure 14-8 TLServer.java

public class TLServer {
    public static String fetchOrder() {
        return "Fetching order for " + TLDBConn.TLUSER.get().name;
    }
}

Figure 14-9 TLApplication.java

import java.util.*;

public class TLApplication {
    
    public void testConnection(User u) {
        
        Runnable r = () -> {
            TLDBConn.TLUSER.set(u);
            TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
            System.out.println(TLServer.fetchOrder());
            
            try {
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
                        
            TLDBConn.TLUSER.set(new User(u.name + " renamed"));
            TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");            
        };
        
        Thread t = new Thread(r, u.name);
        t.start();
        
    }
    
    public static void main(String[] args) {
        
        TLApplication myApp = new TLApplication();
        
        for(int i=0 ; i<5; i++) {
            myApp.testConnection(new User("user" + i));
            try {
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

TLApplication prints output similar to the following:

Thread user0, testConnection: user0
Fetching order for user0
Thread user1, testConnection: user1
Fetching order for user1
Thread user2, testConnection: user2
Fetching order for user2
Thread user0, testConnection: user0 renamed
Thread user1, testConnection: user1 renamed
Thread user2, testConnection: user2 renamed
Thread user3, testConnection: user3
Fetching order for user3
Thread user3, testConnection: user3 renamed
Thread user4, testConnection: user4
Fetching order for user4
Thread user4, testConnection: user4 renamed

Note that even though the member variable TLDBConn.USER is declared as final static, its value is unique for each thread created by TLApplication.

Also, note that the TLServer::fetchOrder method has no parameters. It doesn't require code, in particular, TLApplication::testConnection to pass it a User parameter. TLServer::fetchOrder can directly access the TLDBConn.USER thread-local variable that corresponds to the thread in which it's running:

        return "Fetching order for " + TLDBConn.TLUSER.get().name;

Consequently, thread-local variables enable you to hide method arguments.

Inheriting Thread-Local Variables

When a parent thread starts a child thread, none of the values of the parent thread's thread-local variables are inherited by the child thread. However, if you want a child thread to inherit the values of its parent's thread-local values, then create a thread-local variable with the InheritableThreadLocal class instead.

The following example includes a InheritableThreadLocal variabled named TLADMIN in addition to the ThreadLocal named TLUSER:

Figure 14-10 TLDBConn.java

public class TLDBConn {
    
    final static ThreadLocal<User> TLUSER = new ThreadLocal<>();
    final static InheritableThreadLocal<User> TLADMIN = new InheritableThreadLocal<>();
    
    public static String open(String info) {
        System.out.println(info + ": " + TLUSER.get().name);
        return info + ": " + TLUSER.get().name;
    }
}

The following method starts a thread named childThread within a thread. The thread childThread retrieves the value of the InheritableThreadLocal variable named TLADMIN and attempts to retrieve the value of the ThreadLocal variable named TLUSER:

    public void testConnectionWithInheritableTL(User u) {
        
        Runnable r = () -> {
            TLDBConn.TLUSER.set(u);
            TLDBConn.TLADMIN.set(new User("Admin"));
            TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
            System.out.println(TLServer.fetchOrder());
            
            try {
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            Thread childThread = new Thread(
                () -> {
                    System.out.println("Child thread");
                    System.out.println("TLADMIN: " + TLDBConn.TLADMIN.get().name);
                    try {
                        System.out.println("TLUSER: " + TLDBConn.TLUSER.get().name);
                    } catch (NullPointerException e) {
                        System.out.println("NullPointerException: TLUSER hasn't beet set");
                    }
                }
            );
            childThread.start();
                    
                        
            TLDBConn.TLUSER.set(new User(u.name + " renamed"));
            TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");            
        };
        
        Thread t = new Thread(r, u.name);
        t.start();
    }

When you call this method, the following statement in the instantiation of childThread throws a NullPointerException:

System.out.println("TLUSER: " + TLDBConn.TLUSER.get().name);

The value of the ThreadLocal variable TLUSER hasn't been inherited by childThread. However, the value of the InheritableThreadLocal variable TLADMIN has been inherited by childThread. When childThread is started, it prints the following output:

Child thread
TLADMIN: Admin
NullPointerException: TLUSER hasn't beet set

Issues with Thread-Local Variables

Unfortunately, thread-local variables have some design flaws.

Note:

Scoped Values can address these issues with thread-local variables.

Unconstrained Mutability

Every thread-local variable is mutable. This might make it difficult to discern in your application's code which components update the shared state and in what order. In the example described in Thread-Local Variables, TLApplication::testConnection reassigns TLDBConn.TLUSER with a new value:

        Runnable r = () -> {
            TLDBConn.TLUSER.set(u);
            TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
            System.out.println(TLServer.fetchOrder());
            // ...            
            TLDBConn.TLUSER.set(new User(u.name + " renamed"));
            TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");            
        };

Unbounded Lifetime

The Java runtime retains a thread's incarnation of a thread-local variable for the lifetime of the thread or until code in the thread calls the thread local variable's remove method. If you omit calling this method, then the Java runtime might retain thread data longer than necessary. If you're using a thread pool, then a value in a thread-local variable set in one task might leak into another task. If you have set the value of a thread-local variable multiple times in a thread, then there might not be a clear point when it's safe for the thread to call the remove method, which may cause a long-term memory leak.

Expensive Inheritance

The overhead of thread-local variables may be worse when using large numbers of threads because thread-local variables of a parent thread can be inherited by child threads.