1. Introduction

Scala 3 introduced many new features to the language. Some new features introduced include implicit redesign, export clause,, extension methods, and many more.

In this tutorial, we’ll look at one more newly introduced feature, the @threadUnsafe annotation. This new annotation allows us to overcome performance issues with lazy val in some cases.

2. lazy val in Scala

Scala lets us delay the initialization of a variable by using the lazy val keyword. Let’s look at some sample code:

lazy val scala = "Scala-3"

We used the keyword lazy to declare the variable scala. As a result, the initialization of this variable is delayed until it’s accessed for the first time.

This is very helpful in defining expensive operations that may only sometimes be needed. Furthermore, once initialized, it memorizes the result. This means further access to the same variable immediately returns the previously calculated value.

Scala utilizes thread synchronization under the hood to ensure that the variable is only initialized once, even in multi-threaded environments. As a result, the initialization of the lazy val variable locks the entire surrounding class for modification.

We can verify it with an example:

class LazyValClass {
  val counter = new AtomicInteger(0)
  lazy val syncLazyVal: String = {
    counter.incrementAndGet()
    println("Evaluating lazy val")
    Thread.sleep(3000)
    "scala"
  }
}

We defined a class with lazy val, that takes three seconds to complete. We also defined an AtomicInteger to verify this in the test.

Now, we can create multiple threads and access the same lazy val from both at the same time:

it should "lock the entire class when lazy val is initialized" in {
  val lazyValClass = new LazyValClass()
  val thread1 = new Thread(new Runnable {
    def run() = {
      println("Thread 1 started, accessing lazy val")
      lazyValClass.syncLazyVal
    }
  })
  val thread2 = new Thread(new Runnable {
    def run() = {
      println("Thread 2 started, accessing lazy val")
      lazyValClass.syncLazyVal
    }
  })
  thread1.start()
  thread2.start()
  thread1.join()
  thread2.join()
  lazyValClass.counter.get shouldBe 1 // since it locks, it will not evaluate again, so no increment
}

When we run the above test, we see the statement Evaluating lazy val printed only once:

Lazy val initialisation

Additionally, it only increments the counter value once. We can see that the code block is thread-safe using lazy val.

This avoids multiple initializations of the variable, but at the expense of performance. If the initialization is a long-running operation, all other modifications within this class are blocked until the lazy val is initialized. This could lead to performance overhead in some cases.

3. @threadUnsafe Annotation in Scala 3

In some cases, it might be beneficial to have a lazy evaluation without the additional overhead of synchronization. Locking the entire class might have a more significant performance impact than allowing multiple threads to access a lazy variable concurrently.

As a result, Scala 3 introduced an annotation @threadUnsafe to support this. Marking a lazy val with this annotation avoids locking the entire class during its initialization.

We’ll look at this in action using a similar example as before:

class UnsafeLazyValClass {
  val counter = new AtomicInteger(0)
  @threadUnsafe
  lazy val unsafeLazyVal: String = {
    counter.incrementAndGet()
    println("Evaluating unsafe lazy val")
    Thread.sleep(3000)
    "scala"
  }
}

Now, we can access the lazy val from two separate threads simultaneously:

  it should "NOT lock the entire class when @threadUnsafe annotation is used on lazy val" in {
    val unsafeLazyValClass = new UnsafeLazyValClass()
    val thread1 = new Thread(new Runnable {
      def run() = {
        println("Thread 1 started, accessing lazy val")
        unsafeLazyValClass.unsafeLazyVal
      }
    })
    val thread2 = new Thread(new Runnable {
      def run() = {
        println("Thread 2 started, accessing lazy val")
        unsafeLazyValClass.unsafeLazyVal
      }
    })
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()
    unsafeLazyValClass.counter.get shouldBe 2
    // since no synchronisation, it increments twice
  }

When we execute this, we can see that it prints the statement Evaluating unsafe lazy val twice:

threadUnsafe execution

We need to ensure that we aren’t modifying any class variables from within this lazy val block, as it can cause dirty writes due to multiple threads.

4. Conclusion

In this article, we looked at the newly introduced @threadUnsafe annotation in Scala 3. We also discussed the performance impact of lazy val due to the synchronization overhead.

We can use a combination of lazy val and @threadUnsafe annotation in a Scala 3 application to balance laziness and thread safety.

As always, the sample code used in this article is available over on GitHub.