In the next few posts we’ll be looking at some concurrency related topics. In the last post we looked at escaping object references. In this post we’ll look at ThreadLocal
variables in Java.
Introduction to ThreadLocal Variables
ThreadLocal
variables were introduced in Java 1.2. They have been a standard mechanism for sharing data across methods within the same thread.
ThreadLocal
variables are usually created as private static fields in classes that need to associate some state (such as a user ID or transaction ID) with a specific thread. While it looks like an normal variable, a ThreadLocal
variable has one current value per thread.
Each thread that accesses a ThreadLocal
variable via the get()
or set()
methods has its own, independently initialized copy of that variable. We can think of this as a ThreadLocal
variable storing data inside an internal map using the thread itself as the key. The actual value of the data depends on which thread calls its get()
or set()
methods to read or write the value.
Creating ThreadLocal Variables
TheadLocal
variables allow us to store data that is only accessible by a specific thread. We can create a ThreadLocal
variable using its parameterized constructor as follows:
ThreadLocal<Integer> local = new ThreadLocal<>();
When first created, a ThreadLocal
variable has an initial value of null
.
We can also create a ThreadLocal
by using the static withInitial()
factory method and passing a Supplier
to it:
ThreadLocal<Integer> local = ThreadLocal.withInitial( () -> 123 );
Accessing ThreadLocal Values
We can give a ThreadLocal
variable an initial value either by calling its set()
method, or by overriding its initialValue()
method. This is easy to do in an anonymous inner class as follows:
ThreadLocal<Integer> local = new ThreadLocal<>() {
@Override
protected Integer initialValue() {
return new Integer(123);
}
};
The initialValue()
method will be run the first time a thread calls the get()
method. However, if the thread calls the set()
method before calling get()
, then the initialValue()
method will not be invoked.
We can delete the value from the ThreadLocal
variable by calling the remove()
method. If the variable is read again using the get()
method, the initialValue()
method will be called again.
Example
The example code below has two static/class variables. One is a normal static variable shared by all instances. Any updates to this shared variable can be seen by all the other instances. The other is a static ThreadLocal
variable. Each thread can get/set its own values independent of all the other runnable objects.
Both variables are of type AtomicInteger
. These are mutable objects (unlike the usual class wrappers such as Integer
). The class provides atomic arithmetic operations such as increment, decrement, add and accumulate, as well as the usual java.lang.Number
methods.
import java.util.concurrent.atomic.AtomicInteger;
public class RunnableWithThreadLocalAndShared implements Runnable {
// This static variable is shared between all instances.
private static AtomicInteger shared = new AtomicInteger(1000);
// This is also a static variable, but because it is a ThreadLocal,
// each thread can get/set its own values independent of all the
// other runnable objects.
private static ThreadLocal<AtomicInteger> local = new ThreadLocal<>();
// an initial value for each instance
private final int initial;
// constructor for the initial value
public RunnableWithThreadLocalAndShared(int initial) {
this.initial = initial;
}
@Override
public void run() {
System.out.println("Shared value before increment: " + shared);
shared.incrementAndGet();
System.out.println("Shared value after increment: " + shared);
local.set(new AtomicInteger(initial));
System.out.println("Local value before increment: " + local.get());
local.set(new AtomicInteger(local.get().incrementAndGet()));
System.out.println("Local value after increment: " + local.get());
}
} // end of class
In the Test
class, we create a number of these objects, pass them through to the Thread
constructor and start them up.
public class Test {
public static void main(String args[]) {
new Thread(new RunnableWithThreadLocalAndShared(2000)).start();
new Thread(new RunnableWithThreadLocalAndShared(3000)).start();
new Thread(new RunnableWithThreadLocalAndShared(4000)).start();
new Thread(new RunnableWithThreadLocalAndShared(5000)).start();
}
} // end of class
From the output we can see that the shared static/class variable is updated by each thread, while the ThreadLocal
variables are updated independently. Your output might differ in sequence and order, but by the end of the run, the shared value will have been incremented four times, while each local value will only have been incremented once.
Shared value before increment: 1000
Shared value after increment: 1001
Local value before increment: 2000
Local value after increment: 2001
Shared value before increment: 1001
Shared value after increment: 1002
Local value before increment: 3000
Local value after increment: 3001
Shared value before increment: 1002
Shared value after increment: 1003
Local value before increment: 4000
Local value after increment: 4001
Shared value before increment: 1003
Shared value after increment: 1004
Local value before increment: 5000
Local value after increment: 5001
Problems
Notwithstanding the established use of ThreadLocal
variables, there are a number of problems associated with using them.
-
Unconstrained mutability. Every
ThreadLocal
variable is mutable. This allows any code to change the variable’s value at any time. This can cause potential problems with spaghetti-like data flow. -
Unbounded lifetime. Values can persist longer than necessary. This can result in memory leaks if the
remove()
method isn’t called. In particular, if thread pools are used, the value of aThreadLocal
variable set in one thread could accidentally leak into an unrelated thread. -
Expensive inheritance. Inheriting
ThreadLocal
variables across threads adds significant overhead.ThreadLocal
variables in a parent thread can be inherited by child threads. Each child thread then needs to allocate space forThreadLocal
variables used in the parent thread.
Further Reading and Signing Off
For further details, we should read the online ThreadLocal
API documentation. Java 8 documentation is here, and Java 17 docs are here.
For more on the problems with ThreadLocal
variables, see JEP 481: Scoped Values (Third Preview).
Lots more on concurrency to come!
Was this useful? Please share your comments, and as always, stay safe and keep learning!