Immutability in the highly concurrent environment

We saw how immutability affects the creation and design of programs, so now we will understand how it is useful.

In this section, we will cover the following topics:

  • The cons of mutable collections
  • Creating two threads that simultaneously modify a mutable collection
  • Reasoning about a concurrent program

Let's first understand the cause of mutable collections. To do this, we will be creating two threads that simultaneously modify the mutable collection. We will be using this code for our test. First, we will create a ListBuffer that is a mutable list. Then, we can add and delete links without creating another list for any modification. We can then create an Executors service with two threads. We need two threads to start simultaneously to modify the state. Later, we will use a CountDownLatch construct from Java.util.concurrent:. This is shown in the following example:

import java.util.concurrent.{CountDownLatch, Executors}
import org.scalatest.FunSuite
import scala.collection.mutable.ListBuffer
class MultithreadedImmutabilityTest extends FunSuite {

test("warning: race condition with mutability") {
//given
var listMutable = new ListBuffer[String]()
val executors = Executors.newFixedThreadPool(2)
val latch = new CountDownLatch(2)

The CountDownLatch is a construct that helps us to stop threads from processing until we request them to. We need to wait with our logic until both threads start executing. We then submit a Runnable to the executors and our run() method does the countDown() by signaling when it is ready for action and appends "A" to listMutable, as shown in the following example:

 //when
executors.submit(new Runnable {
override def run(): Unit = {
latch.countDown()
listMutable += "A"
}
})

Then, another thread starts, and also uses countDown by signaling that it is ready to start. But first, it checks whether the list contains "A" and, if not, it appends that "A", as shown in the following example:

 executors.submit(new Runnable {
override def run(): Unit = {
latch.countDown()
if(!listMutable.contains("A")) {
listMutable += "A"
}
}
})

We then use await() to wait until countDown is issued and, when it is issued, we are able to progress with the verification of our program, as shown in the following example:

    latch.await()

listMutable contains "A" or it can have "A","A". listMutable checks if the list contains "A" and, if not, it will not add that element, as shown in the following example:

    //then
//listMutable can have ("A") or ("A","A")
}
}

But there is a race condition here. There could be a possibility that, after the check if(!listMutable.contains("A")), the run() thread will add the "A" element to the list. But we are inside if, so we will add another "A" by/using listMutable += "A". Because of the mutability of the state and the fact that it was modified via another thread, we can have "A" or "A","A".

We need to be careful while using mutable state since we cannot have such a corrupted state. To alleviate this problem, we can use java.util collections and synchronized lists on it.

But if we have the synchronized block, then our program will be very slow because we will need to coordinate access to that exclusively. We can also employ lock from the java.util.concurrent.locks package. We can use an implementation, like ReadLock or WriteLock. In the following example, we will use WriteLock:

val lock = new WriteLock()

We also need to lock our lock() and only then proceed, as shown in the following example:

lock.lock()

Later, we can use unlock(). However, we should also do this in the second thread so that our list only has one element, as shown in the following example:

lock.unlock()

The output is as follows:

Locking is a very hard and expensive operation, and so immutability is key to performance programs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.245.167