Synchronization in Java (with Example)
We know that multithreading increases the speed of execution of a program and optimizes computer resource usage. Normally, multiple threads in a single program run asynchronously if threads do not share a common resource.
Under some circumstances, it is possible that more than one thread access the common resource (shared resource). That is, more than one thread access the same object of a class.
In this case, care must be taken by the programmer that only one thread use shared resource at a time. The technique through which the solution to this problem is achieved is called synchronization in Java.
In other words, when a thread is already accessing an instance of a class, preventing any other thread from acting on the same instance concurrently is called thread synchronization in Java or thread safe.
The object on which threads are synchronized is called synchronized object. The synchronization of thread is recommended when multiple threads are accessing the same object. That is, when you are working in multi-threading.
The primary purpose (objective) of thread synchronization is to control the access and use of shared resources in a multithreaded environment. Java provides a keyword named “synchronized” that helps to synchronize thread.
Let’s understand better the need for the synchronization technique by taking realtime examples.
Realtime Example of Synchronization in Java
1. Suppose a thread in a program is reading a record from a file while another thread is still writing the same file. In this situation, the program may produce undesirable output.
2. Let’s take a scenario of railway reservation system where two passengers are trying to book seats from Dhanbad to Delhi in Rajdhani Express. Both passengers are trying to book tickets from different locations.
Now suppose that both passengers start their reservation process at 11 am and observe that only two seats are available. First passenger books two seats and simultaneously the second passenger books one seat.
Since the available number of seats is only two but booked seats are three. This problem happened due to asynchronous access to the railway reservation system.
In this realtime scenario, both passengers can be considered as threads, and the reservation system can be considered as a single object, which is modified by these two threads asynchronously.
This asynchronous problem is known as race condition in which multiple threads access the same object and modify the state of object inconsistently.
The solution to this problem can be solved by a synchronization mechanism in which when one thread is accessing the state of object, another thread will wait to access the same object at a time until their come turn.
Object Lock in Java
The code in Java program can be synchronized with the help of a lock. A lock has two operations: acquire and release. The process of acquiring and releasing object lock (monitor) of an object is handled by Java runtime system (JVM).
In Java programming language, every object has a default object lock that can be used to lock on a thread. This object lock is also known as monitor that allows only one thread to use the shared resources (objects) at a time.
To acquire an object lock on a thread, we call a method or a block with the synchronized keyword. Before entering a synchronized method or a block, a thread acquires an object lock of the object.
On exiting synchronized method or block, thread releases the object’ monitor lock. A thread can again acquire the object’ monitor lock as many times as it wants.
Object lock is like a room with only one door. A person enters the room and locks the door from inside. The second person who wants to enter the room will wait until the first person come out.
Similarly, a thread also locks the object after entering it, the next thread cannot enter it until the first thread comes out. Since the object is locked mutually on threads, therefore, this object is called mutex (mutually exclusive lock).
Look at the below figure to understand thread synchronization in Java better.
Thus, when a thread acquires a lock associated with an object to access shared resources, other thread waits to acquire lock associated with the object at that time.
Rules for Synchronizing Shared Resources in Java
In the Java program, there must mainly three rules to be followed when synchronizing share resources. The rules are as follows:
1. A thread must get an object lock associated with it before using a shared resource. If a thread has an object lock of the shared resource, Java runtime system (JVM) will not allow another thread to access the shared resource.
If a thread is trying to access the shared resource at the same time, it is blocked by JVM and has to wait until the resource is available.
2. Only methods or blocks can be synchronized. Variables or classes cannot be synchronized.
3. Only one lock is associated with each object or shared resource.
Types of Synchronization in Java
We mostly use synchronization in Java programming to prevent thread interference. There are two types of synchronization in Java that are as:
- Process synchronization
- Thread synchronization
1. Process Synchronization:
This type of synchronization occurs between processes that operate independently. It is primarily used in distributed systems or when multiple processes need to share resources without blocking each other.
For example, web browsers like Google Chrome and Mozilla Firefox run as individual processes. Each tab in these browsers operates as a separate process, which allows them to function independently. If one tab crashes, it doesn’t necessarily affect the performance of others.
2. Thread Synchronization:
Thread synchronization is the process of controlling concurrent execution of the critical resources between multiple threads. This is the most relevant form of synchronization in Java because it manages the access to shared resources between multiple threads. We can further divide the thread synchronization into two categories:
- Mutual Exclusion (Mutex)
- Cooperation (Inter-thread communication)
a) Mutual Exclusion (Mutex):
In mutual exclusion, only one thread can access a critical section of a resource at any given time. We can achieve mutual exclusion using:
- Synchronized method
- Synchronized block
b) Inter-thread Communication:
This type of synchronization allows multiple threads to communicate with each other. We can achieve inter-thread communication in Java using methods like wait(), notify(), and notifyAll() within synchronized blocks.
You will learn more about these topics in further tutorial with examples one by one.
How can We Achieve Synchronization in Java?
There are two ways by which we can achieve or implement synchronization in Java. That is, we can synchronize the object. They are:
- Synchronized method
- Synchronized block
When we declare a synchronized keyword in the header of a method, it is called synchronized method. Once a method is made synchronized, and thread calls the synchronized method, it gets locked on the method.
All other threads will have to wait for the current thread to release the lock. Using the synchronized keyword, we can synchronize the entire method. For example, if you want to synchronize the code of show() method, add the synchronize before the method name. The syntax to make method synchronized is as follows:
synchronized void show() { // statements to be synchronized }
Since the code inside the method is synchronized, the code will not be available to more than one thread at a time.
Synchronizing a block of code is another way of controlling the execution of thread. The general syntax to make a block of code synchronized is as follows:
synchronized(object) { // statements to be synchronized }
Here, object is a reference variable of an object that has to be locked or synchronized. The statements within the synchronized block are available to only one thread at a time.
In the further tutorial, we will learn programs based on synchronized method and synchronized block.
Why Use Synchronization?
The synchronization is mainly used to prevent thread interference and consistency problem. In a multi threaded environment more than one thread can try to modify a shared resource at the same time. Without synchronization threads can interfere with each other and result in unpredictable outcomes including:
- Race condition: A race condition occurs when more than one thread access the same object and modifies the state of object inconsistently.
- Data inconsistency: When multiple threads read and write shared variables without proper synchronization, it can result to incorrect or inconsistent data.
Synchronization is the mechanism to prevent these issues by controlling thread access to shared resources. Let’s take an example in which two threads will try to increment a shared variable without synchronization.
Example:
public class Counter {
private int count = 0;
// Declare a method to increment the count.
public void increment() {
count++;
}
// Declare a method to get the current value of count.
public int getCount() {
return count;
}
}
public class CounterThread extends Thread {
private Counter counter;
// Declare a constructor to pass the shared Counter object.
public CounterThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
// Increment the count 500 times.
for (int i = 0; i < 500; i++) {
counter.increment();
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Create two threads that share the same Counter object.
CounterThread thread1 = new CounterThread(counter);
CounterThread thread2 = new CounterThread(counter);
// Start both threads.
thread1.start();
thread2.start();
// Wait for both threads to complete their execution.
thread1.join();
thread2.join();
// Output the result
System.out.println("Final count: " + counter.getCount());
}
}
Output: Final count: 763
In this example, we have defined a class named Counter, which contains a shared variable count and two instance methods. We have declared a increment() method to increment the variable count by 1. The getCount() method returns the current value of count.
Then, we have declared a class named CounterThread, which extends Thread class. This class is responsible for incrementing the Counter object in its run() method. It calls the increment() method 500 times.
After that, we have declared a class named Main in which we have declared the main method. Inside the main() method, we have created two threads, such as thread1 and thread2, sharing the same Counter object. When both threads start, they run concurrently, attempting to increment the shared count variable.
The thread1.join() and thread2.join() methods tell the main thread to pause its execution and wait until both thread1 and thread2 have completed their tasks. The main thread will not move forward and print the final result until both threads have finished their execution.
Finally, after both threads complete their execution, the value of the count variable is displayed on the console.
The problem here is that without proper synchronization, both threads are calling the increment() method at the same time. This can lead to inconsistent or incorrect results because the count variable is not being updated correctly.
Since the threads are accessing and modifying the shared count variable simultaneously, a race condition occurs. As a result, the final value of count may vary each time you run the program.
If you run the code two or three times, you’ll likely get different outputs, which indicates the presence of a race condition or data inconsistency. To fix this issue, you would need to use synchronization. We will understand it in the next tutorial.
Key Points to Remember
Here, we have listed some key points related to Java thread synchronization that you keep in mind.
1. There are two types of synchronization: process synchronization and thread synchronization.
2. Thread synchronization in Java program is achieved through the monitor (lock) concept.
3. A monitor is an object that can block and resume threads.
4. Every object in Java programming language has an associated monitor.
5. Java programming language supports only two kinds of threads synchronization: Mutual exclusion synchronization and Conditional synchronization.
6. In mutual exclusion synchronization, only a single thread is allowed to have access to code at a time.
7. In conditional synchronization, multiple threads are allowed to work together to get results.
In this tutorial, you learned thread synchronization in Java through realtime examples. I hope that you will have understood the basic definition of synchronization and how to achieve it in Java. Stay tuned with the next tutorial where you will understand synchronized method in Java with examples.
Thanks for reading!!!