How to Use synchronized() and withLock() to Lock Shared Resources in Kotlin

David Mbochi Njonge Feb 02, 2024
  1. Generate a Kotlin Project
  2. Thread Interference in Action
  3. Lock Shared Resources Using the synchronized() Function in Kotlin
  4. Lock Shared Resources Using the withLock() Function in Kotlin
  5. Conclusion
How to Use synchronized() and withLock() to Lock Shared Resources in Kotlin

When working with multithreaded applications, we need to ensure that access to shared resources is managed to avoid inconsistent data. Synchronization is the approach that we usually use when we want to control access to shared resources in our applications.

Synchronization is realized using locks, ensuring no thread accesses the shared resource until the currently executing thread has finished running. A thread must first obtain an object’s lock before it accesses the shared resource and release it once done.

There are different ways locks can be created, and in this tutorial, we will learn how locks are made in Kotlin to control access to shared resources.

Generate a Kotlin Project

Open IntelliJ development environment and select File > New > Project. On the window that opens, enter the project name as kotlin-withlock, select Kotlin on the Language section, and select Intellij as the Build System. Finally, press the Create button to generate the project.

Once the application has been generated, create a file named Main.kt under the src/main/kotlin folder. We will use this file to implement all the code examples for this tutorial.

Thread Interference in Action

Thread interference occurs in a multithreaded application without control over how shared resources are accessed. The following code shows how the thread interference occurs in a multithreaded application.

Copy and paste the following code into the Main.kt file.

class Calculator {
    fun addIntArray() {
        var initialValue = 0;
        for (number in 1..5) {
            println(
                Thread.currentThread().name
                        + " ---> " + initialValue + " + "
                        + number + " = "
                    .plus(String.format("%d", (initialValue + number)))
            );
            initialValue += number;

            Thread.sleep(500);
        }
    }
}

fun main() {
    val calculator = Calculator();

    Thread {
        calculator.addIntArray();
    }.start();

    Thread {
        calculator.addIntArray();
    }.start();
}

In this code, we have created a method named addIntArray() inside a class named Calculator. This method is a shared resource in our code, and since we will be creating multiple threads, we have added a println() method that allows us to log the currently executing thread to the console.

When the sleep() method is invoked, the currently executing thread is suspended for 500 milliseconds, and another thread starts running. This means that the threads access our method simultaneously, which can cause data inconsistency.

In the main() method, we created two threads and called the start() method on them to start executing the addIntArray() method of the calculator object we have created.

Run this code and note how the threads are interleaved, as shown below.

Thread-0 ---> 0 + 1 = 1
Thread-1 ---> 0 + 1 = 1
Thread-0 ---> 1 + 2 = 3
Thread-1 ---> 1 + 2 = 3
Thread-1 ---> 3 + 3 = 6
Thread-0 ---> 3 + 3 = 6
Thread-1 ---> 6 + 4 = 10
Thread-0 ---> 6 + 4 = 10
Thread-0 ---> 10 + 5 = 15
Thread-1 ---> 10 + 5 = 15

Lock Shared Resources Using the synchronized() Function in Kotlin

This example uses the addIntArray() method we have implemented in the previous section to show how access to it is managed. Comment out the main() method created in the previous example and copy and paste the following code into the Main.kt file after the comment.

val calculator = Calculator();

fun sumUsingSynchronization(): Unit = synchronized(calculator) {
    calculator.addIntArray();
}

fun main() {

    Thread {
       sumUsingSynchronization();
    }.start();

    Thread {
        sumUsingSynchronization()
    }.start();
}

The main objective of this tutorial is to learn how locks are created to ensure exclusive access of threads to a shared resource. By default, objects have implicit monitor locks that threads use to achieve synchronization, and this code uses an implicit monitor lock by passing the Calculator object to the synchronized() function.

The synchronized() function is invoked by the sumUsingSynchronization() function which in turn invoke the addIntArray() method. In the main() method, we created two threads and invoked the start() method on them to start the execution.

Note that since our code uses an implicit monitor lock, there is no interference between the two threads. The first thread uses the addIntArray() method exclusively until it has finished executing before the second thread starts.

The synchronized() function is not only limited to the implicit monitor locks but can also use any other locks such as the ReentrantLock, and we only need to pass the lock as the argument of this method. Run this code and ensure the output is as shown below.

Thread-0 ---> 0 + 1 = 1
Thread-0 ---> 1 + 2 = 3
Thread-0 ---> 3 + 3 = 6
Thread-0 ---> 6 + 4 = 10
Thread-0 ---> 10 + 5 = 15
Thread-1 ---> 0 + 1 = 1
Thread-1 ---> 1 + 2 = 3
Thread-1 ---> 3 + 3 = 6
Thread-1 ---> 6 + 4 = 10
Thread-1 ---> 10 + 5 = 15

Lock Shared Resources Using the withLock() Function in Kotlin

Comment out the main() method created on the previous example and copy and paste the following code into the Main.kt file after the comment.

import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

val reentrantLock: Lock = ReentrantLock();

fun sumUsingLock(): Unit = reentrantLock.withLock {
    calculator.addIntArray();
}

fun main() {

    Thread {
       sumUsingLock();
    }.start();

    Thread {
        sumUsingLock()
    }.start();
}

In this code, we have created a method named sumUsingLock() that is assigned to the withLock() extension function of the Lock interface. The withLock() extension function uses the provided lock implementation to execute the code block exclusively for each of the multiple threads created.

In this example, we created a custom lock using ReentrantLock, which implements the Lock interface. This lock is used by the threads created to execute the addIntArray() method exclusively.

Run this code and ensure the output is as shown below.

Thread-0 ---> 0 + 1 = 1
Thread-0 ---> 1 + 2 = 3
Thread-0 ---> 3 + 3 = 6
Thread-0 ---> 6 + 4 = 10
Thread-0 ---> 10 + 5 = 15
Thread-1 ---> 0 + 1 = 1
Thread-1 ---> 1 + 2 = 3
Thread-1 ---> 3 + 3 = 6
Thread-1 ---> 6 + 4 = 10
Thread-1 ---> 10 + 5 = 15

Conclusion

In this tutorial, we have learned how to manage access to shared resources by covering how locks are created on these resources. We covered how interference occurs, how locks are used with the synchronized() function, and how locks are used with the withLock() function.

Note that the difference between these two approaches is that the synchronized() function is not limited to any lock implementation, but the withLock() function is limited to the Lock implementation.

David Mbochi Njonge avatar David Mbochi Njonge avatar

David is a back end developer with a major in computer science. He loves to solve problems using technology, learning new things, and making new friends. David is currently a technical writer who enjoys making hard concepts easier for other developers to understand and his work has been published on multiple sites.

LinkedIn GitHub