In the previous topic, you've learned what thread synchronization is, why it is important, and how we can realize it in Kotlin. Now, let's see what a monitor is and how we use it to implement thread synchronization.
Example: a synchronized counter
Let's take a look at an example and remember how we do synchronization in code. It's a synchronized counter with two synchronized methods: increment and getValue.
class SynchronizedCounter {
var count = 0
@Synchronized
fun increment() {
count++
}
@Synchronized
fun getValue(): Int {
return count
}
}
When multiple threads invoke increment on the same instance, no problem arises because the annotation @Synchronized protects the shared field. Only one thread can change the field – other threads will wait until the thread releases the monitor. All changes of the variable count are visible.
The method getValue doesn't modify the field – it only reads the current value. The method is synchronized so that the reading thread always reads the actual value; otherwise, there is no guarantee that the reading thread will see the updated value of count after it's changed.
Here is a class called WorkerThread that extends Thread. The class takes an instance of SynchronizedCounter and calls the method increment 10 000 000 times.
class WorkerThread(val counter: SynchronizedCounter) : Thread() {
override fun run() {
for (i in 1..10_000_000) {
counter.increment()
}
}
}
The following code creates an instance of SynchronizedCounter, starts threads, and prints the result.
fun main() {
val counter = SynchronizedCounter()
val worker1 = WorkerThread(counter)
val worker2 = WorkerThread(counter)
worker1.start()
worker2.start()
worker1.join()
worker2.join()
println(counter.getValue()) // the result is 20_000_000
}
Sometimes, however, there's no need to synchronize the methods that only read shared data (including getters):
If we have a guarantee that the thread successfully returns from
joinwhen it reads a field. That's true about the code above, and we can remove thesynchronizedannotation from the declaration ofgetValue.
If a shared field is declared with the
@Volatileannotation. In that case, all writes to this field are immediately made visible to other threads.
Monitor and multithreading
From the previous example, you can see that synchronization allows us to protect critical sections from simultaneous changes by multiple threads. In the previous topic, we already briefly mentioned the monitor.
The monitor is a special mechanism in Kotlin to control concurrent access to objects.
In Kotlin JVM, each object has an associated implicit monitor. So, when a thread captures some object, it blocks other threads' access to the object's monitor. Therefore, multiple threads cannot acquire an object's monitor at the same time. A monitor can only be acquired by one thread, others will wait until the owner (the thread that acquired the monitor) releases it.
Thus, a thread can be locked by the monitor of an object and wait for its release. This mechanism allows programmers to protect critical sections from being accessed by multiple threads concurrently.
However, this mechanism can lead to a deadlock when several processes are in a state of waiting for resources occupied by each other and none of them can continue their execution. So, you should use synchronization carefully and know how the blocking monitor works in different situations.
One monitor and multiple synchronized methods and blocks
Important: an object has only one monitor, and only one thread can execute synchronized code on the same monitor.
It means that if a class has several synchronized methods and a thread invokes one of them, other threads cannot execute either of these methods on the same instance until the first thread releases the monitor of the instance.
So, the annotation @Synchronized and the function synchronized() lock the monitor of the object to which the linked function or block belongs. If we have two instances of a class, each instance has a monitor for synchronizing.
Here is an example: a class with three methods. Two methods are synchronized and the third one has an internal synchronized block. Both methods and the block are synchronized on the monitor of this instance.
class SomeClass {
@Synchronized
fun method1() {
// do something useful
}
@Synchronized
fun method2() {
// do something useful
}
fun method3() {
synchronized(this) {
// do something useful
}
}
}
If a thread invokes method1 and executes the statements inside the method, no other thread can execute the statements inside method2 or in the synchronized block in method3 because this monitor is already acquired. The threads will wait for the release of the monitor.
The same behavior applies when a class monitor is used.
Reentrant synchronization
A thread cannot acquire a lock owned by another thread – but it can acquire a lock that it already owns. This behavior is called reentrant synchronization.
Take a look at the following example:
class SomeClass {
@Synchronized
fun method1() {
method2() // legal invocation because the thread has acquired the monitor of SomeClass
}
@Synchronized
fun method2() {
// do something useful
}
}
The code above is correct. When a thread is inside method1, it can invoke method2 because both methods are synchronized on the same object (SomeClass).
Fine-grained synchronization
Sometimes, a class has several fields that are never used together. It's possible to protect these fields by using the same monitor, but in this case, only one thread will be able to access one of these fields, despite their independence. To improve the concurrency rate, it's possible to use an idiom with additional objects as monitors.
Here is an example: a class with two methods. The class stores the number of calls to each method in a special field.
class SomeClass {
var numberOfCallingMethod1 = 0
var numberOfCallingMethod2 = 0
val lock1 = Any() // an object for locking
val lock2 = Any() // another object for locking
fun method1() {
println("method1...")
synchronized(lock1) {
numberOfCallingMethod1++
}
}
fun method2() {
println("method2...")
synchronized(lock2) {
numberOfCallingMethod2++
}
}
}
As you can see, the class has two additional fields that are the locks for separating monitors for each critical section.
If we have an instance of the class, one thread may work inside the synchronized block of the first method and, at the same time, another thread may work inside the synchronized block of the second method.
Conclusion
In this topic, we've learned the ways of implementing the mechanism of thread synchronization. Remember, code protected by the synchronization mechanism can be executed only by one thread at a time. That reduces the parallelism and responsiveness of the program.
Do not synchronize all your code. Try to use synchronization only when it is really necessary. Determine small parts of code to be synchronized. Sometimes, if a method is complex, it's better to use a synchronization block instead of synchronizing the whole method.