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.