1. Introduction
Synchronization is an important aspect of concurrent programming. It allows multiple threads to access shared resources without stomping on each other. There are several ways to handle this in Kotlin, and we’ll look at Lock.withLock() vs the synchronized keyword to achieve synchronization. While both approaches serve the same purpose, they have differences in syntax and behavior that are worth examining.
In this tutorial, we’ll explore these synchronization techniques and see what makes them different.
2. The synchronized Keyword
The synchronized keyword in Kotlin provides a simple way to ensure that a particular block of code is run by one thread at a time. It’s a direct translation of the Java synchronized keyword. Because of this, it only works with threads and not coroutines.
Non-blocking coroutine functions like delay() won’t work because a coroutine isn’t thread-bound, and therefore can’t be blocked with this approach.
2.1. Syntax
There are two ways we can utilize the synchronized keyword in Kotlin.
First, we can use it as a function:
object ObjectToLockOn
fun toSynchronized() = synchronized(ObjectToLockOn) {
println("Synchronized function call")
}
The synchronized block takes an object as a parameter. The code inside the block uses ObjectToLockOn as the lock, ensuring that only one thread can enter the synchronized block at a time.
Second, we can use the @Synchronized annotation:
@Synchronized
fun synchronizedMethod() {
println("Synchronized function call")
}
In this case, we use the @Synchronized annotation to mark a method as synchronized. Similarly, it ensures that only one thread can execute this method simultaneously.
When we annotate a method with @Synchronized, the Kotlin compiler automatically generates the necessary synchronization code for us. Specifically, it wraps the entire method body in a synchronized block, using the instance of the class as the lock.
2.2. Usage
To demonstrate the usage of the synchronized keyword, let’s consider the following code:
class CounterClass {
private var count = 0
@Synchronized
fun incrementAndGet(): Int {
return ++count
}
@Synchronized
fun getCount(): Int {
return count
}
}
We annotate the incrementAndGet() and getCount() methods with @Synchronized. This ensures that only one thread can execute either of these methods at a time, preventing concurrent access issues.
Finally, let’s test this to ensure it works as we expect:
@Test
fun `test synchronized keyword usage on a class`() {
val counter = CounterClass()
val threads = List(500) { index ->
thread {
val value = counter.incrementAndGet()
assertEquals(index + 1, value)
}
}.forEach { it.join() }
assertEquals(500, counter.getCount())
}
This unit test creates 500 threads, each calling the incrementAndGet() method on the CounterClass instance. Each thread expects a value matching its predetermined order.
3. Using Lock.withLock()
The Lock.withLock() extension function is part of Kotlin’s standard library. It provides fine-grained control over synchronization and is particularly useful when we need more flexibility in our locking strategy.
3.1. Syntax
We’ll use the Lock.withLock() extension function to enclose the code we want to synchronize:
val lockObject = ReentrantLock()
fun performSynchronizedOperation(){
lockObject.withLock {
println("Synchronized function call")
}
}
The Lock interface from the java.util.concurrent.locks package represents a mutually exclusive lock that can be used to lock critical sections of code by ensuring that only one thread can execute the locked section at a time.
When we invoke the withLock() extension function on a Lock object, it ensures that the code block executes while holding the lock, thus providing thread-safe access to the code within the block.
3.2. Usage
Let’s revisit our counter from the earlier example and convert it to use the Lock interface for synchronization instead:
class LockCounterClass {
private var count = 0
private val lock = ReentrantLock()
fun incrementAndGet(): Int {
lock.withLock {
count++
return count
}
}
fun getCount(): Int {
return lock.withLock {
count
}
}
}
The LockCounterClass class uses a ReentrantLock to control access to the incrementAndGet() and getCount() methods. The ReentrantLock is a concrete implementation of the Lock interface that provides reentrant locking capabilities, meaning that the same thread can acquire the lock multiple times without causing a deadlock.
Consequently, the withLock() extension function ensures these code sections execute one thread at a time.
Lastly, to ensure correctness, let’s test this similar to before:
@Test
fun `test withLock method usage`() {
val counter = LockCounterClass()
val threads = List(500) { index ->
thread {
val value = counter.incrementAndGet()
assertEquals(index + 1, value)
}
}.forEach { it.join() }
assertEquals(500, counter.getCount())
}
This unit test also creates 500 threads, each calling the incrementAndGet() method on the LockCounterClass instance. Each thread expects the method to return the value matching its predetermined order. Finally, the join() method ensures that the main thread waits for all created threads to finish their execution before moving on.
4. Conclusion
In this article, we’ve discussed the synchronization techniques available in Kotlin, specifically focusing on the synchronized keyword and the Lock.withLock() extension function.
We saw how synchronized offers a straightforward approach to ensure that a block of code or method is executed by only one thread at a time, making it ideal for simpler scenarios. In contrast, Lock.withLock() provides greater flexibility and control over synchronization, which is suitable for more complex locking strategies.
As always, the code used in this article is available over on GitHub.