Multithreading — synchronization, race conditions, class level and method level synchronization.

Java Jedi
7 min readNov 24, 2022

Synchronization is one of the most essential and challenging concepts in multithreading. It ensures the consistency of shared data between threads. Since threads share the same memory, they access data from shared memory and race conditions happen. Race condition is a situation when two or more threads try to access the shared data at the same time and it leads to data corruption and inconsistency. Synchronization solves this problem. Let’s first talk about race conditions.

Race conditions

Let’s consider the following example and see how data inconsistency occurs due to race conditions.

In this example, we have a static variable — counter = 0 and a static method — increment() that counts till 100_000 and increases the value of counter by 1. In the main() method we create two threads and each of them call the increment() method. If we run this program, we will get the following output:

Output

We got the value — 127973, however we expect to get 200000 since two threads are executing and each counts and increases the counter value by 1. Running this program will give a different output each time in a range of [100000 — 200000]. This happens due to the following:

Increment function in multithreaded environment

As illustrated above, increment() is executed in three steps:
- reading the value from memory.
- incrementing the value by 1.
- saving the new value into memory.

As we discussed about time-slicing algorithm, threads get processing time and execute for that amount of time. As soon as the processing time expires, another threads starts to execute and values of the current thread are preserved until it gets its processing time to continue execution.

Data inconsistency happens when thread-1 reads the value (counter = 1), increments (counter = 1 + 1) and processing time of thread-1 expires. Thread-1 has no enough time to save incremented value back to memory and the value is still equal to 1. Then, thread-2 starts executing, it reads the value (counter = 1), increments it (counter = 1 + 1), saves the value (counter = 2) back into memory and thread-2 finishes execution. After thread-1 continues its execution from where it was stopped and reads its preserved values. The only step for thread-1 left — saving the value back to memory since it has already incremented the value. Thread-1 saves the value into memory and finishes its execution. The counter = 2. Thread-1 does not know about thread-2 execution and that the value has already been incremented (counter = 2) and saved into memory by thread-2. The value = 2 was saved twice by two threads. However, we expected the value = 3 since two threads executed increment() method. This is how race condition and data inconsistency happen. Next, we will talk about synchronization to prevent data inconsistency.

Synchronization

Synchronization is a technique that ensures that two or more threads or processes cannot execute some critical section or access critical data. When one thread starts executing, no other thread cannot execute or access shared data, hence it provides data consistency.

Speaking about the example above if synchronization was applied, when thread-1 starts execution, thread-2 cannot start execution and will wait until thread-1 finishes fully. Thread-2 starts execution after thread-1 and it increments the value from 2 to 3 since thread-1 has incremented the value from 1 to 2. Hence, data consistency is achieved.

Synchronization in Java

In Java, synchronization is built around an internal entity known as an intrinsic lock or monitor lock. Intrinsic locks ensure the exclusive access to object. In Java every object has an intrinsic lock associated with it. When a thread needs an inclusive and consistent access to objects, it has to acquire the locks of the objects and release when it is done with them. A thread is said to own the lock of an object between the time it acquires and releases the lock. As long as a thread owns the lock, no other thread can acquire it at the same time.

Synchronization in Examples

In Java synchronized keyword is used to achieve synchronization. The synchronized keyword can be used either in method signature or inside the method.

Using synchronized keyword in method signature is called — synchronized method. It synchronizes the whole method.

 public static synchronized void increment() { // acquiring the lock of the class
for (int i = 0; i < 100_000; i++) {
counter++;
}
}

public synchronized void increment() { // acquiring the lock of the instance of the class
for (int i = 0; i < 100_000; i++) {
counter++;
}
}

And using synchronized keyword inside a method is called — synchronized statement. It synchronizes only the part that need sync instead of the whole method.

// synchronized (App.class) - class level locking
// since we acquire the lock of the class
public static void increment() {
synchronized (App.class) {
for (int i = 0; i < 100_000; i++) {
counter++;
}
}
}

// synchronized (this) - object level locking
// since we acquire the lock of the object.
// this - refers to the instance of the current class
public void increment() {
synchronized (this) { // acquiring the lock of the instance of the class
for (int i = 0; i < 100_000; i++) {
counter++;
}
}
}

Using synchronized statements is preferable than synchronized methods.

Since in Java static methods are associated with the class and not the instances of the class, there is an intrinsic lock associated with the class as well. When two threads access static method of the class, they acquire the lock of the class, not the instance of the class.

Let’s use synchronized keyword in the example discussed before.

We changed method increment() signature and turned into synchronized method. Running this program will give the correct result. When two threads start execution, only one thread acquires the lock at a given time and executes method increment(), whereas the other thread will be in a blocking state. As soon as the thread releases the lock, the other thread starts execution. Data consistency is achieved.

Output:

Drawbacks of synchronization

The problem with synchronization is that every object has only one intrinsic lock. Let’s assume that we have two independent synchronized methods, then the threads have to wait for each other to release the lock.

The problem of synchronization — two distinct synchronized method with one class

In this example, there are two synchronized methods — increment() and decrement(). This program starts two threads — thread-1 invokes increment() and thread-2 invokes decrement(). Whenever thread-1 acquires the lock, thread-2 will wait for thread-1 to release the lock and vice-versa. Threads will wait for each other since there is only one lock per class. Thus, it makes the performance slower.

However, if thread-1 calls a sync method and thread-2 calls a non-sync method, the program will not face such issues and will be faster, since threads will not wait for each other and execute independently.

Locking with custom objects

Since there is only one lock per class or object, it impossible for two threads to invoke two different synchronized methods independently. Since one thread will own the lock at a time, another thread will wait for the lock to be released. To solve this issue, it is good practice to make use of custom object as lock object. Every object has an intrinsic lock associated with it and we can create custom objects per methods. Let’s consider the following example:

Locking with custom objects

This example is nearly the same the last one we have discussed. Two distinct synchronized methods within one class — increment() and decrement(). However, in this case, threads will not wait for each other for the intrinsic lock of the class, since the methods are synchronized using the intrinsic locks of custom objects— incrementLock and decrementLock.

private final static Object incrementLock = new Object(); //used as lock object

private final static Object decrementLock = new Object(); //used as lock object

Whenever increment() or decrement() method is invoked, a thread acquires the lock of incrementLock or decrementLock object and releases the lock after. Hence, two threads can execute these methods independently.

In this series of post in Multithreading, we have discussed all the necessary concepts related to synchronization, how to achieve synchronization and drawbacks of synchronization. Next, we will discuss inter-thread communication — wait and notify. Subscribe and stay tuned to be the first to read new posts.

--

--