9 minutes read

Concurrent use of shared data by multiple threads may cause unexpected or erroneous behavior. Fortunately, Kotlin allows us to control the access of multiple threads to a shared resource of any type. The solution is thread synchronization.

Important terms and concepts

Before we start using synchronization in our code, let's introduce the necessary terms and concepts.

1) Thread synchronization is a mechanism which ensures that two or more concurrent threads do not simultaneously execute a code segment called a critical section.

2) A critical section is a region of code that accesses shared resources and should not be executed by more than one thread at the same time. A shared resource may be a variable, file, input/output port, database, or something else.

Let's consider an example. Let some class Counter have a field named count:

class Counter {
    var count = 0

    fun inc() {
        count++
    }
}

Two threads concurrently increment the field (increase it by 1) 10 000 000 times.

import kotlin.concurrent.thread

fun main() {
    val counterInstance = Counter()
    val thread1 = thread(block = {
        for (i in 1..10_000_000) {
            counterInstance.inc();
        }
    })
    val thread2 = thread(block = {
        for (i in 1..10_000_000) {
            counterInstance.inc();
        }
    })
    thread1.join()
    thread2.join()
    println("The result of the threads' work: ${counterInstance.count}")
}

The final value should be 20 000 000. However, when the program ends its work, we have a different, wrong result, for example, 18 696 438.

The result of the threads' work: 18696438

This happens because sometimes a thread does not see the changes of shared data made by another thread, and sometimes a thread may see an intermediate value of a non-atomic operation. Those are examples of visibility and atomicity problems we deal with while working with shared data.

That is why increasing a value by multiple threads is a critical section. Of course, this example is very simple, and a critical section may be way more complex.

Code synchronizing

The "classic" and simplest way to protect code from being concurrently accessed by multiple threads involves using synchronized methods.

There are two different forms of synchronization:

@Synchronized fun myFunction() {
    //do something
}

Note that annotations are a means of attaching metadata to code; in the following topics, you will learn more about them. For now, just remember that @Synchronized is an easy way to tell the compiler that a method can only be used by one thread at a time.

  • synchronized blocks or statements (we use the function synchronized()):

 fun myOtherFunction() {
 
    // a synchronized block
    synchronized(this) {
         // block's code
    }
}

A synchronized function or block needs an object for locking threads. Only one thread can execute code in a synchronized block or method at the same time. Other threads are blocked until the thread inside the synchronized block or method exits it.

Let's take a look at examples of synchronized blocks and functions and discuss some important aspects of protecting your code from being accessed by multiple threads concurrently.

Synchronized functions

Now then, functions are synchronized by the annotation @Synchronized.

Only one thread can execute code in a synchronized method of a particular instance. Meanwhile, different threads can execute methods of different objects at the same time. This can be summarized as "one thread per instance".

Here is an example of a class with a single synchronized method named doSomething(). The class also has a constructor for distinguishing instances.

class SomeClass(val className: String) {
    @Synchronized
    fun doSomething() {
        val threadName = Thread.currentThread().name
        println("$threadName entered the method of $className")
        println("$threadName leaves the method of $className")
    }
}

We have a class MyThread – it extends the Thread class and tries to call the method doSomething() of the instance which it takes as a parameter.

class MyThread(val classInstance: SomeClass) : Thread() {
    override fun run() {
        classInstance.doSomething()
    }
}

Let's create two instances of the class and three threads invoking doSomething(). The first and second threads take the same instance of the class, and the third thread takes a different one.

val instance1 = SomeClass("instance-1")
val instance2 = SomeClass("instance-2")

val first = MyThread(instance1)
val second = MyThread(instance1)
val third = MyThread(instance2)

first.start()
second.start()
third.start()

The result will look like this:

Thread-0 entered the method of instance-1
Thread-2 entered the method of instance-2
Thread-0 leaves the method of instance-1
Thread-1 entered the method of instance-1
Thread-2 leaves the method of instance-2
Thread-1 leaves the method of instance-1

As you can see, there are no threads executing the code in doSomething of instance-1 at the same time. Try running it several times.

Synchronized blocks (statements)

Sometimes, you need to synchronize only a part of a method. This is possible with the function synchronized().

Here is a class with the changeValue() method, which is unsynchronized but has a synchronized part inside it, where we change the value of the Someclass instance.

class SomeClass {
    var value = 0
    fun changeValue(newValue: Int) {

        // unsynchronized code
        print("I'd like to change the value for $newValue")
        synchronized(this) { // synchronization on the class
            // synchronized code
            value = newValue
        }
        print("The value has been changed successfully!")
    }
}

The block inside changeValue() is synchronized on this instance, which means that only one thread can change the value of the instance. However, some other thread is able to change the value of another instance at the same time.

Synchronized blocks may resemble synchronized methods, but they allow programmers to synchronize only the necessary part of a method.

Conclusion

In this topic, you've learned about one of the most important mechanisms in multithreading – thread synchronization. This mechanism allows you to ensure the correct working of your code by protecting data from simultaneous changes by multiple threads.

Also, you've learned how you can implement thread synchronization in code:

  • synchronize functions by using the annotation @Synchronized;

  • synchronize blocks or statements by using the function synchronized().

In the next topic, you'll learn what a monitor is and how thread synchronization works in Kotlin.

57 learners liked this piece of theory. 3 didn't like it. What about you?
Report a typo