In Scala, synchronization is a way to coordinate access to shared resources between multiple threads to ensure that only one thread can modify the resource at a time. Synchronization is achieved through the use of synchronization primitives, which are objects that provide the necessary mechanisms to enforce synchronization.
One synchronization primitive in Scala is the `synchronized` keyword, which can be used to synchronize access to a block of code. The `synchronized` keyword takes an object as a parameter, and ensures that only one thread can execute the block of code at a time while holding a lock on the object. For example, consider the following code that uses the `synchronized` keyword to synchronize access to a shared counter:
scala class Counter { private var count = 0 def increment(): Unit = synchronized { count += 1 } def getCount(): Int = synchronized { count } } val counter = new Counter() val thread1 = new Thread { override def run(): Unit = { for (i <- 1 to 10000) { counter.increment() } } } val thread2 = new Thread { override def run(): Unit = { for (i <- 1 to 10000) { counter.increment() } } } thread1.start() thread2.start() thread1.join() thread2.join() println(counter.getCount()) // prints 20000
In this example, a `Counter` class isdefined with a private `count` variable and two methods `increment()` and `getCount()` that increment and return the value of the counter, respectively. Both methods are marked as `synchronized` to ensure that only one thread can access the counter at a time.
Two new threads are then created that each increment the counter 10000 times. The `start()` method is called on each thread to execute their respective blocks of code concurrently with the main program. The `join()` method is then called on each thread to wait for them to finish executing, and the final value of the counter is printed to the console.
Another synchronization primitive in Scala is the `Lock` class, which provides a more flexible way to synchronize access to shared resources. The `Lock` class provides methods like `lock()` and `unlock()` that allow you to explicitly acquire and release a lock on a resource. For example, consider the following code that uses the `Lock` class to synchronize access to a shared buffer:
scala import java.util.concurrent.locks.ReentrantLock class Buffer { private val lock = new ReentrantLock() private var data: List[Int] = List() def add(value: Int): Unit = { lock.lock() try { data = value :: data } finally { lock.unlock() } } def remove(): Option[Int] = { lock.lock() try { if (data.nonEmpty) { valvalue = data.head data = data.tail Some(value) } else { None } } finally { lock.unlock() } } } val buffer = new Buffer() val producer = new Thread { override def run(): Unit = { for (i <- 1 to 10000) { buffer.add(i) } } } val consumer = new Thread { override def run(): Unit = { var sum = 0 while (sum < 50005000) { buffer.remove() match { case Some(value) => sum += value case None => } } println(sum) } } producer.start() consumer.start() producer.join() consumer.join()
In this example, a `Buffer` class is defined with a private `lock` variable of type `ReentrantLock` and two methods `add()` and `remove()` that add and remove elements from the buffer, respectively. Both methods acquire the lock on the `lock` object before modifying the buffer, and release the lock after they are done.
A new `Buffer` object is created, and two new threads are created: a producer thread that adds numbers from 1 to 10000 to the buffer, and a consumer thread that removes numbers from the buffer and computes their sum until the sum of the first 10000 numbers is reached. The `start()` method is called on each thread to executetheir respective blocks of code concurrently with the main program, and the `join()` method is called on each thread to wait for them to finish executing.
Overall, synchronization is an important concept in concurrent programming, and Scala provides several synchronization primitives, including the `synchronized` keyword and the `Lock` class, to help coordinate access to shared resources between threads. However, it is important to use these primitives carefully to avoid issues like deadlocks and livelocks, and to consider using higher-level abstractions like actors and futures for more complex concurrency scenarios.