The main characteristic of a lazy val is that the bound expression is not evaluated immediately, but once on the first access. When the initial access happens, the expression is evaluated and the result bound to the identifier of the lazy val. On subsequent access, no further evaluation occurs: instead the stored result is returned immediately.
Given the characteristic above, using the lazy modifier seems like an innocent thing to do, when we are defining a val, why not also add a lazy modifier as a speculative “optimization”? In a moment we will see why this is typically not a good idea, but before we dive into this, let’s recall the semantics of a lazy val first.
When we assign an expression to a lazy val like this:
lazy val two: Int = 1 + 1
we expect that the expression 1 + 1 is bound to two, but the expression is not yet evaluated. On the first (and only on the first) access of two from somewhere else, the stored expression 1 + 1 is evaluated and the result (2 in this case) is returned. On subsequent access of two, no evaluation happens: the stored result of the evaluation was cached and will be returned instead.
This property of “evaluate once” is a very strong one. Especially if we consider a multithreaded scenario: what should happen if two threads access our lazy val at the same time? Given the property that evaluation occurs only once, we have to introduce some kind of synchronization in order to avoid multiple evaluations of our bound expression. In practice, this means the bound expression will be evaluated by one thread, while the other(s) will have to wait until the evaluation has completed, after which the waiting thread(s) will see the evaluated result.
How is this mechanism implemented in Scala? Luckily, we can have a look at SIP-20. The example class LazyCell with a lazy val value is defined as follows:
final class LazyCell {
lazy val value: Int = 42
}
A handwritten snippet equivalent to the code the compiler generates for our LazyCell looks like this:
final class LazyCell {
@volatile var bitmap_0: Boolean = false // (1)
var value_0: Int = _ // (2)
private def value_lzycompute(): Int = {
this.synchronized { // (3)
if (!bitmap_0) { // (4)
value_0 = 42 // (5)
bitmap_0 = true
}
}
value_0
}
def value = if (bitmap_0) value_0 else value_lzycompute() // (6)
}
At (3) we can see the use of a monitor this.synchronized {...} in order to guarantee that initialization happens only once, even in a multithreaded scenario. The compiler uses a simple flag ((1)) to track the initialization status ((4) & (6)) of the var value_0 ((2)) which holds the actual value and is mutated on first initialization ((5)).
What we can also see in the above implementation is that a lazy val, other than a regular val has to pay the cost of checking the initialization state on each access ((6)). Keep this in mind when you are tempted to (try to) use lazy val as an “optimization”.
Now that we have a better understanding of the underlying mechanisms for the lazy modifier, let’s look at some scenarios where things get interesting.