1. Overview
Scala provides a nice language feature called lazy val that defers the initialization of a variable. The lazy initialization pattern is common in Java programs.
Though it seems tempting, the concrete implementation of lazy val has some subtle issues. In this quick tutorial, we’ll examine the lazy val feature by analyzing its underlying bytecode.
2. How Does a lazy val Work ?
To designate a val as lazy, we simply need to add the lazy keyword in front of the variable declaration:
lazy val foo = {
println("Initialized")
1
}
foo: Int = <lazy>
The compiler does not immediately evaluate the bound expression of a lazy val. It evaluates the variable only on its first access.
Upon initial access, the compiler evaluates the expression and stores the result in the lazy val. Whenever we access this val at a later stage, no execution happens, and the compiler returns the result.
Let’s see the output we get when we run this program:
Initialized
res0: Int = 1
3. Decoding a lazy val
Next, let’s find out what’s going on inside a lazy val.
First, we’ll declare a single lazy val inside a Person class:
class Person {
lazy val age = 27
}
When we compile our Person.scala file, we will get a Person.class file. We can decompile this class file using any java decompiler. Once we decompile this class file, we get the equivalent Java code that gets generated for every lazy val:
public class Person {
private int age;
private volatile boolean bitmap$0;
private int age$lzycompute() {
synchronized (this) {
if (!this.bitmap$0) {
this.age = 27;
this.bitmap$0 = true;
}
}
return this.age;
}
public int age() {
return this.bitmap$0 ? this.age : this.age$lzycompute();
}
}
That’s quite a lot of code, considering what we started with!
On line 7, the compiler introduces a monitor synchronized (this) {…}. This guarantees that the variable initializes only once. The compiler introduces a boolean flag bitmap$0 is to track the initialization status. This variable gets mutated when the compiler accesses the lazy val for the first time.
4. Performance Bottlenecks of lazy vals
4.1. Potential Deadlock When Accessing lazy vals
When we access multiple vals inside an instance from multiple threads, there’s always a potential for deadlock to occur:
object FirstObj {
lazy val initialState = 42
lazy val start = SecondObj.initialState
}
object SecondObj {
lazy val initialState = FirstObj.initialState
}
object Deadlock extends App {
def run = {
val result = Future.sequence(Seq(
Future {
FirstObj.start
},
Future {
SecondObj.initialState
}
))
Await.result(result, 10.second)
}
run
}
On running the above code, we get the stack strace:
Exception in thread "main" java.util.concurrent.TimeoutException: Future timed out after [10 seconds]
at scala.concurrent.impl.Promise$DefaultPromise.tryAwait0(Promise.scala:212)
at scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:225)
at scala.concurrent.Await$.$anonfun$result$1(package.scala:201)
at scala.concurrent.BlockContext$DefaultBlockContext$.blockOn(BlockContext.scala:62)
At line 18, the Future initializes FirstObj, and the instance of FirstObj internally tries to initialize SecondObj. Also, the Future at line 21 tries to initialize the SecondObj. This results in a potential deadlock situation.
4.2. Sequential Evaluation of lazy vals Inside Object
Let’s declare lazy vals inside an object and try to access them concurrently. In such cases lazy vals get executed sequentially.
object LazyValStore {
lazy val squareOf5 = println(square(5))
lazy val squareOf6 = println(square(6))
def square(n: Int): Int = n * n
}
object SequentialLazyVals extends App {
def run = {
val result = Future.sequence(Seq(
Future {
LazyValStore.squareOf5
},
Future {
LazyValStore.squareOf6
}
))
Await.result(result, 15.second)
}
run
}
The compiler introduces a monitor for every lazy val. Due to this, compiler locks the whole instance during initialization. When multiple threads try to access the instance, threads have to wait till all lazy vals get initialized.
5. Conclusion
In this tutorial, we explored the pitfalls of Scala’s lazy val. We’re tempted to optimize our code by replacing vals with lazy vals. However, we must be completely aware of the implications that lazy val brings to the table.
As always, the complete code is available over on GitHub.