Synchronizing Threads in Java: A Practical Guide

Synchronizing Threads in Java: A Practical Guide

6 mins read682 Views Comment
Updated on Oct 3, 2023 11:45 IST

Synchronization in java is used to remove the data inconsistency when multiple threads access the same object or resource simultaneously. In this article, we will briefly discuss different types of thread synchronization in Java.

2023_01_MicrosoftTeams-image-131.jpg

In this article, we will briefly discuss the synchronization of Thread in Java with the help of examples. But before going deeper to know more about synchronization of thread, you must have a clear understanding of threads and multithreading in Java.

When multiple threads access the same object simultaneously, then there may be data inconsistency. To resolve this data inconsistency problem synchronized method or synchronized block should be used. If a method or block is synchronized, then only one thread at a time is allowed to execute that method or block on a given object. This is what thread synchronization in java means.

Must Check: What is Java?

Must Check: Top Java Online Courses & Certifications

Types of Thread Synchronization in Java

There are two types of Thread synchronization in Java:

Now, letโ€™s discuss them, one by one in a complete detail:

Recommended online courses

Best-suited Java courses for you

Learn Java with these high-rated online courses

โ€“ / โ€“
350 hours
Free
6 months
โ€“ / โ€“
4 months
โ€“ / โ€“
โ€“ / โ€“
โ€“ / โ€“
1 month
โ‚น40 K
7 weeks
โ‚น7.8 K
3 months
โ‚น8.47 K
2 months
โ‚น7 K
6 months
โ‚น4 K
2 months

Mutual Exclusive

Method level Synchronization

Suppose multiple threads operate on the same method, and that method is not synchronized. In that case, a data inconsistency problem will occur, so to overcome this problem, we make a synchronized method.

Syntax

 
datatype synchronized method
void synchronized print(){ // method is synchronized
// lines of codes
}
Copy code

With the help of a program, letโ€™s look at what challenges we will face when the method is not synchronized.

Example:

In the below program, class Table contains one method print() which is not synchronized. Two threads, t1 and t2 are accessing the same print() method, which print table 4 and 6, respectively. Letโ€™s see the output we get from the program when the method is not synchronized.

Code

 
class Table{
void print(int val){ // method is not synchronized
// In this for loop , table of 4 and 6 is print.
for(int i=1;i<=10;i++){
System.out.println(val*i);
try{
Thread.sleep(1000);
}catch(Exception e){
System.out.println(e);
}
}
}
}
//Thread 1 access print() method
class Thread1 extends Thread{
Table t;
Thread1(Table t){
this.t=t;
}
public void run(){
// access print() method of Table class and pass 4 as argument
t.print(4);
}
}
//Thread2 access print() method
class Thread2 extends Thread{
Table t;
Thread2(Table t){
this.t=t;
}
public void run(){
// access print() method of Table class and pass 6 as argument
t.print(6);
}
}
public class Main
{
public static void main(String[] args) {
Table t=new Table();
Thread1 t1=new Thread1(t);
Thread2 t2=new Thread2(t);
t1.start();
t2.start();
}
}
Copy code

In the above program, class Thread1 and Thread2 extends the Thread class. In Thread1 class, the print() method of Table class is called to print table of 4 and in the Thread2 class, the print() method of the Table class is called to print a table of 6. Both classes access the print() method simultaneously. Since the method is not synchronized, we get different outputs.

Output

2023_01_image-81.jpg

Here, by observing the outputs, we conclude that if the method is not synchronized, then we get different outputs, or a data inconsistency problem will occur. So, to resolve this issue, we have to make a synchronized method. Now letโ€™s make the method synchronized and then see the output.

Code

 
class Table{
synchronized void print(int val){ // method is synchronized
// In this for loop , table of 4 and 6 is print.
for(int i=1;i<=10;i++){
System.out.println(val*i);
try{
Thread.sleep(1000);
}catch(Exception e){
System.out.println(e);
}
}
}
}
//Thread 1 access print() method
class Thread1 extends Thread{
Table t;
Thread1(Table t){
this.t=t;
}
public void run(){
// access print() method of Table class and pass 4 as argument
t.print(4);
}
}
//Thread2 access print() method
class Thread2 extends Thread{
Table t;
Thread2(Table t){
this.t=t;
}
public void run(){
// access print() method of Table class and pass 4 as argument
t.print(6);
}
}
public class Main
{
public static void main(String[] args) {
Table t=new Table();
Thread1 t1=new Thread1(t);
Thread2 t2=new Thread2(t);
t1.start();
t2.start();
}
}
Copy code

Output:

2023_01_image-82.jpg

Here, the print() method is synchronized, so at a time, only one thread is allowed to execute the method. Hence, we get the same output, and thus, we solve the problem of data inconsistency that we saw in the earlier program when the method is not synchronized. The first table of 4 is printed, and then the table of 6 will print.

Block Level Synchronization

In block-level synchronization, the entire method is not synchronized; only the part of the method or the block of code in the method is synchronized, which allows one thread to execute that block of code at a time.

Syntax

 
synchronized(this){
//lines of code
}
Copy code

Also Read: This Keyword in Java

Example:

Letโ€™s take the same example again of table printing, but instead of making the method synchronized, we make a synchronized block inside the print () method to print tables 4 and 6. We always get the same output as the thread is synchronized, and at a time, one thread enters inside the synchronized block to execute the code.

Code:

 
class Table{
void print(int val){
synchronized(this){ // block is synchronized
for(int i=1;i<=10;i++){
System.out.println(val*i);
try{
Thread.sleep(1000);
}catch(Exception e){
System.out.println(e);
}
}
}
}
}
//Thread 1 access print() method
class Thread1 extends Thread{
Table t;
Thread1(Table t){
this.t=t;
}
public void run(){
t.print(4);
}
}
//Thread2 access print() method
class Thread2 extends Thread{
Table t;
Thread2(Table t){
this.t=t;
}
public void run(){
t.print(6);
}
}
public class Main
{
public static void main(String[] args) {
Table t=new Table();
Thread1 t1=new Thread1(t);
Thread2 t2=new Thread2(t);
t1.start();
t2.start();
}
}
Copy code

Here, we also get the same output that we get in the synchronized method.

Output:

2023_01_image-83.jpg

Static Synchronization of Thread

As we know, in Java, each object has one lock, which means at a time, one thread can execute a block of code on a given instance of the class. So, if we have multiple objects in our program, then multiple locks are there, and if we are dealing with multiple threads in our program, then the output may be corrupt, and a data inconsistency problem will occur. To resolve such a problem, we use a static synchronized block or method.

Example

Letโ€™s assume Shubham, Vikram, Inder, and Kamlesh have a joint account in a bank. The total balance in the bank account is 10000 rupees. If Shubham or Vikram withdraws 10000, then the amount withdrawn by any of the rest is zero or insufficient balance is shown to them, but Inder or Kamlesh is still able to withdraw 10000 rupees, and vice-versa, this shows the program is incorrect and inconsistent or corrupt data is given by the program.
Go through the below program to get clarity on the above mentioned example.
In the below program, two objects are created b1 and b2. For one object, one lock is there, which means at a time one thread is allowed to execute the method, so, for two objects, two locks are there, which allow two threads to execute the same method simultaneously; hence we get corrupt output, or 20000 amount is withdrawn in spite of having only 10000 amounts in Bank.

Code

 
class Bank extends Thread{
int available_balance=10000;
int withdrawl_balance;
Bank(int withdrawl_balance){
this.withdrawl_balance=withdrawl_balance;
}
public synchronized void withdraw(){ // method is synchronized
String thread_name=currentThread().getName();
if(withdrawl_balance<=available_balance){
System.out.println(thread_name+""+" withdraw money:"+""+withdrawl_balance);
available_balance-=withdrawl_balance;
}else{
System.out.println(thread_name+" your account has Insufficient balance");
}
}
public void run(){
withdraw();
}
}
public class Main
{
public static void main(String[] args) {
Bank b1=new Bank(10000); // has one lock
Thread t1=new Thread(b1);
t1.setName("Shubham");
Thread t2=new Thread(b1);
t2.setName("Vikram");
t1.start();
t2.start();
Bank b2=new Bank(10000);// has one lock
Thread t3=new Thread(b2);
Thread t4=new Thread(b2);
t3.setName("Inder");
t4.setName("Kamlesh");
t3.start();
t4.start();
}
}
Copy code

Output:

2023_01_image-84.jpg

Now, if we make method static synchronized then one thread execute method at a time and we get consistent data.

Code:

 
class Bank extends Thread{
static int available_balance=10000;
static int withdrawl_balance;
Bank(int withdrawl_balance){
this.withdrawl_balance=withdrawl_balance;
}
public static synchronized void withdraw(){ // method is static synchronized
String thread_name=currentThread().getName();
if(withdrawl_balance<=available_balance){
System.out.println(thread_name+""+" withdraw money:"+""+withdrawl_balance);
available_balance-=withdrawl_balance;
}else{
System.out.println(thread_name+" your account has Insufficient balance");
}
}
public void run(){
withdraw();
}
}
public class Main
{
public static void main(String[] args) {
Bank b1=new Bank(10000); // has one lock
Thread t1=new Thread(b1);
t1.setName("Shubham");
Thread t2=new Thread(b1);
t2.setName("Vikram");
t1.start();
t2.start();
Bank b2=new Bank(10000);// has one lock
Thread t3=new Thread(b2);
Thread t4=new Thread(b2);
t3.setName("Inder");
t4.setName("Kamlesh");
t3.start();
t4.start();
}
}
Copy code

Output:

2023_01_image-86.jpg

Inter-Thread Communication

Two threads can communicate with each other by using wait(), notify(), and notifyall() methods. The thread which is expecting updation is responsible for calling the wait() method and then immediately entering into the waiting state. The thread which is responsible for performing the updation call notify() method so that the waiting thread gets a notification and can continue its execution with those updated items.
Example:
In the below program, the Main thread communicates with the child thread to print the sum of the first ten natural numbers. Here main thread expects an updation of the sum, and the child thread performs an updation. Also note that wait(), notify(), and notifyall() methods must be put inside the synchronized block; otherwise, the program gets IllegalMonitorStateException.

Code:

 
class Thread1 extends Thread{
int sum=0;
public void run(){
synchronized(this){
for(int i=1;i<=10;i++){
sum+=i;
}
//child thread send notification to Main thread
this.notify();
}
}
}
public class Main
{
public static void main(String[] args) throws Exception {
Thread1 t=new Thread1();
t.start();
synchronized(t){
//Main thread calling wait() method
t.wait();
//Main Thread get notification
System.out.println(t.sum);
}
}
}
Copy code

Output

2023_01_image-87.jpg

Conclusion

In this article, we have discussed:

  • What is Synchronizing thread in Java?
  • Types of Synchronizing thread with examples

Hope you will like the article.

Contributed By: Shubham Kumar

About the Author

This is a collection of insightful articles from domain experts in the fields of Cloud Computing, DevOps, AWS, Data Science, Machine Learning, AI, and Natural Language Processing. The range of topics caters to upski... Read Full Bio