Mutexes in Scala

In the previous example, we saw how to manage simple counter state using atomic operations. For more complex state, we can use a mutex to safely access data across multiple threads.

import scala.collection.mutable
import java.util.concurrent.locks.ReentrantLock

// Container holds a map of counters; since we want to
// update it concurrently from multiple threads, we
// add a ReentrantLock to synchronize access.
// Note that locks must not be copied, so if this
// class is passed around, it should be done by
// reference.
class Container {
  private val lock = new ReentrantLock()
  private val counters = mutable.Map[String, Int]("a" -> 0, "b" -> 0)

  // Lock before accessing counters; unlock
  // it at the end of the function using a try-finally block.
  def inc(name: String): Unit = {
    lock.lock()
    try {
      counters(name) += 1
    } finally {
      lock.unlock()
    }
  }

  def getCounters: Map[String, Int] = counters.toMap
}

object MutexExample extends App {
  val c = new Container()

  // This function increments a named counter
  // in a loop.
  def doIncrement(name: String, n: Int): Runnable = () => {
    for (_ <- 1 to n) {
      c.inc(name)
    }
  }

  // Run several threads concurrently; note
  // that they all access the same Container,
  // and two of them access the same counter.
  val threads = List(
    new Thread(doIncrement("a", 10000)),
    new Thread(doIncrement("a", 10000)),
    new Thread(doIncrement("b", 10000))
  )

  threads.foreach(_.start())
  threads.foreach(_.join())

  println(c.getCounters)
}

Running the program shows that the counters updated as expected.

$ scala MutexExample.scala
Map(a -> 20000, b -> 10000)

Next, we’ll look at implementing this same state management task using only actors and message passing.

In this Scala version:

  1. We use ReentrantLock from java.util.concurrent.locks instead of Go’s sync.Mutex.
  2. The Container class encapsulates the lock and the counters.
  3. We use a try-finally block to ensure the lock is always released, similar to Go’s defer.
  4. Instead of goroutines, we use Java threads, which Scala can utilize.
  5. We replace Go’s WaitGroup with explicitly joining the threads at the end.
  6. The doIncrement function is defined as a method returning a Runnable, which is used to create threads.

This example demonstrates how to use mutexes in Scala to safely access shared state from multiple threads, achieving the same goal as the original Go program.