You can also check out my Youtube Channel to get access to various tutorials created by me.
This article will focus on what the synchronized keyword does in Java and when to use it.
In order to understand this article better please go through my article on Introduction to Multithreading in case you are new to multithreading in Java.
What is the use of Synchronized?
Let’s take the following code snippet:
public void doSomething(){
//Execute Logic 1
//Execute Logic 2
......
}
Let’s say that 2 threads t1 and t2 will be calling doSomething() method.
Here the main thing we care about is that if one thread calls doSomething(), then it needs to finish everything in the method before another thread gets to call doSomething().
Basically something like this:
t1 executes Logic 1
t1 executes Logic 2
t2 executes Logic 1
t2 executes Logic 2
or
t2 executes Logic 1
t2 executes Logic 2
t1 executes Logic 1
t1 executes Logic 2
But reality is they can run in any order.. The below scenario is highly possible:
t1 executes Logic 1
t2 executes Logic 1
t1 executes Logic 2
t2 executes Logic 2
But what if we want to ensure that t1 fully finishes doSomething() before t2 starts doSomething()?
This is where synchronized comes in. Synchronized ensures that any method or piece of code which is synchronized should be fully completed by one thread before another thread can access that block.
If we make doSomething() synchronized as shown below, then only one thread can access doSomething() at a time
public synchronized void doSomething(){
//Execute Logic 1
//Execute Logic 2
......
}
Different ways of using Synchronized
Normal Synchronized Method
In this section we will create 3 threads which will print numbers from 0 to 5.
First let’s create a simple utility which will print numbers from 0 to 5:
class PrintUtil {
public synchronized void printNumbers() {
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
This util has a method called printNumbers(). This method prints numbers from 0 to 5 and waits for 100ms after every print. The method is marked as synchronized so that at any time only one thread can access printNumbers() method.
Next let’s create a worker which will implement Runnable Interface:
class RunnableWorker implements Runnable {
PrintUtil pu;
public RunnableWorker(PrintUtil pu) {
this.pu = pu;
}
@Override
public void run() {
pu.printNumbers();
}
}
This is very simple worker which calls the printNumbers() method in PrintUtil class.
Finally let’s create the Main class which will tie all this together:
public class SynchronizedMethodDemo {
public static void main(String[] args) {
PrintUtil pu = new PrintUtil();
Runnable r = new RunnableWorker(pu);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
}
}
The main class creates an instance of PrintUtil.
It then creates 3 threads t1, t2 and t3 by passing the RunnableWorker as an argument and then starts the 3 threads.
The output of the above program is given below:
Thread-0: 0
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-0: 4
Thread-0: 5
Thread-2: 0
Thread-2: 1
Thread-2: 2
Thread-2: 3
Thread-2: 4
Thread-2: 5
Thread-1: 0
Thread-1: 1
Thread-1: 2
Thread-1: 3
Thread-1: 4
Thread-1: 5
As seen above, Synchronized has ensured that only when one thread completes printing from 0 to 5, the other thread starts it’s execution.
How does this work ?
In this example the PrintUtil object instance pu has a lock. When one thread executes the printNumbers() method, it gets the lock from pu. This thread releases the lock only when printNumbers() execution is fully done. The other threads can execute printNumbers() only when they get the lock.
Since each thread holds the lock until printNumbers() is fully complete, we get the output shown above.
In General the lock is held by the object which calls the synchronized method. In this case that object was pu
Synchronized with Static methods
In this example we use a static method to print numbers from 0 to 5 instead of an instance method.
The Printer Utility code is shown below:
class PrintUtilExample2 {
public static synchronized void printNumbers() {
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Here the method is marked as static and synchronized.
Next let us create the worker. The code for the worker is shown below:
class RunnableWorkerExample2 implements Runnable {
@Override
public void run() {
PrintUtilExample2.printNumbers();
}
}
In this case the worker calls printNumbers() directly using the class name since it is static method.
Finally we have the main class which is shown below:
public class SynchronizedStaticMethodDemo {
public static void main(String[] args) {
Runnable r = new RunnableWorkerExample2();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
}
}
Here also the main method creates three threads t1, t2 and t3 with argument as RunnableWorkerExample2 and starts the threads.
The output of this code is given below:
Thread-2: 0
Thread-2: 1
Thread-2: 2
Thread-2: 3
Thread-2: 4
Thread-2: 5
Thread-0: 0
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-0: 4
Thread-0: 5
Thread-1: 0
Thread-1: 1
Thread-1: 2
Thread-1: 3
Thread-1: 4
Thread-1: 5
Again we can see that each thread completes printing from 0 to 5 before the next thread starts printing.
How does this work
In case of static methods, the lock is maintained by the PrintUtilExample2 class itself. Rest of the locking mechanism is exactly same as mentioned in the previous section.
Creating synchronized blocks with custom objects
In this section we will see how to make a block of code synchronized using a custom object.
The Print utility class is shown below:
class PrintUtilExample3 {
final Object lockObject = new Object();
public void printNumbers() {
synchronized(lockObject) {
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Here the printNumbers() method is not marked as synchronized.
We create an object called lockObject. We then create a synchronized block of code using synchronized(lockObject){}
The advantage in using this approach, is that we can mark any custom block of code as synchronized. ( It can be an entire method, or it can be even a single line in the entire method. )
The worker code is shown below:
class RunnableWorkerExample3 implements Runnable {
PrintUtilExample3 pu3;
public RunnableWorkerExample3(PrintUtilExample3 pu3){
this.pu3 = pu3;
}
@Override
public void run() {
pu3.printNumbers();
}
}
The main class is shown below:
public class SynchronizedCustomObjectDemo {
public static void main(String[] args) {
PrintUtilExample3 pu3 = new PrintUtilExample3();
Runnable r = new RunnableWorkerExample3(pu3);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
}
}
The output of the above program is shown below:
Thread-0: 0
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-0: 4
Thread-0: 5
Thread-2: 0
Thread-2: 1
Thread-2: 2
Thread-2: 3
Thread-2: 4
Thread-2: 5
Thread-1: 0
Thread-1: 1
Thread-1: 2
Thread-1: 3
Thread-1: 4
Thread-1: 5
In this approach as well we see that each thread completes printing from 0 to 5 before the next thread starts printing the numbers.
How does this work
In this case the custom object created lockObject holds the lock. Rest of the locking mechanism is same as mentioned in the previous sections.
Also in the previous sections the entire method was marked as synchronized. But in this approach any block of code can be marked as synchronized which gives a greater flexibility.
Code
All the code discussed in this article can be found in this GitHub repo.
Issues with Synchronized
Synchronized does come with some issues. There maybe scenarios where we want a block of code to be synchronized only for write operations. But for read operations any number of concurrent threads should be allowed. In order to handle these scenarios there are other better approaches in Java which I will cover in a future articles.
Congrats 😊
You now know what is synchronized and how to use it in Java.