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.