1. Introduction
Before we introduced thread-safety, and how it can be achieved.
In this article, we’ll take a look at local variables and why they are thread-safe.
2. Stack Memory and Threads
Let’s start with a quick recap of the JVM memory model.
Most importantly, the JVM splits up its available memory into stack and heap memory. Firstly, it stores all objects on the heap. Secondly, it stores local primitives and local object references on the stack.
In addition, it’s important to realize that every thread, including the main thread, has its own private stack. Therefore, other threads do not share our local variables, which is what makes them thread-safe.
3. Example
Let’s now continue with a small code example containing a local primitive and a (primitive) field:
public class LocalVariables implements Runnable {
private int field;
public static void main(String... args) {
LocalVariables target = new LocalVariables();
new Thread(target).start();
new Thread(target).start();
}
@Override
public void run() {
field = new SecureRandom().nextInt();
int local = new SecureRandom().nextInt();
System.out.println(field + ":" + local);
}
}
On line five, we instantiate one copy of the LocalVariables class. On the next two lines, we start two threads. Both will execute the run method of the same instance.
Inside the run method, we update the field field of the LocalVariables class. Secondly, we see an assignment to a local primitive. Finally, we print the two fields to the console.
Let’s take a look at the memory location of all the fields.
First, the field is a field of class LocalVariables. Therefore, it lives on the heap. Secondly, the local variable number is a primitive. Consequently, it’s located on the stack.
The println statement is where things could go wrong when running the two threads.
First, the field field has a high probability of causing trouble since both the reference and the object live on the heap and are shared between our threads. The primitive local will be okay since the value lives on the stack. Consequently, the JVM does not share local between threads.
So when executing we could, for example, have the following output:
821695124:1189444795
821695124:47842893
In this case, we can see that we indeed had a collision between the two threads. We affirm this since it’s highly unlikely that both threads generated the same random integer.
4. Local Variables Inside Lambdas
Lambdas (and anonymous inner classes) can be declared inside a method and can access the method’s local variables. However, without any additional guards, this could lead to a lot of trouble.
Before JDK 8, there was an explicit rule that anonymous inner classes could only access final local variables. JDK 8 introduced the new concept of effectively final, and the rules have been made less strict. We’ve compared final and effectively final before and we’ve also discussed more on effectively final when using lambdas.
The consequence of this rule is that fields that are accessed inside lambdas have to be final or effectively final (they are thus not changed) which makes them thread-safe because of immutability.
We can see this behavior in practice in the following example:
public static void main(String... args) {
String text = "";
// text = "675";
new Thread(() -> System.out.println(text))
.start();
}
In this case, uncommenting the code on line 3 will cause a compilation error. Because then, the local variable text is no longer effectively final.
5. Conclusion
In this article, we looked at the thread-safety of local variables and saw that this is a consequence of the JVM memory model. We also looked into the usage of local variables in combination with lambdas. The JVM guards their thread-safety by demanding immutability.
As always, the full source code of the article is available over on GitHub.