Chapter 43: Synchronization in Java – Managing Concurrency and Thread Communication(Part 5)
Table of contents
- Section 1: Synchronized Block Examples
- Working of Synchronized Blocks:
- Example 1: Synchronized Block in Action
- Explanation of the Example:
- Thread Execution:
- Output:
- Conclusion:
- Example-2: Knowing Which Thread is Executing Outside the Synchronized Block
- Example-3: Knowing Which Thread is Getting Lock and Which Thread is Releasing Lock
- Example-4: Multiple Threads Operating on Multiple Objects
- Example-5: Class-Level Lock Applied
- 2. Scenario to Understand Inter-Thread Communication
- Inter-Thread Communication in Java
- Understanding wait(), notify(), and notifyAll() Methods in Java
- 1. wait() Method
- Illustrative Example of wait() Method:
- 2. notify() Method
- 3. notifyAll() Method
- Summary of Methods:
- Understanding notify(), notifyAll(), and Thread Ownership
- 2. notify() and notifyAll() Methods
- Thread Ownership and Synchronization Context
- Summary of Key Concepts:
- Illustrative Example:
- Important Points on Inter-Thread Communication in Java
- Thread Class Methods: yield(), sleep(), join()
- Object Class Methods: wait(), notify(), notifyAll()
- Key Differences Between yield(), sleep(), join(), and wait(), notify(), notifyAll()
- Why Are wait(), notify(), notifyAll() in the Object Class, Not Thread Class?
- Summary of Key Concepts:
- Thread Life Cycle When a Thread Calls wait() Method
- Thread Life Cycle States:
- Important Transitions from Waiting State:
- Summary of Thread Flow with wait() Method:
- Inter-Thread Communication Example Explanation
- Key Concepts in This Example:
- Code Breakdown:
- Step-by-Step Execution:
- Final Output:
- Thread Scheduler:
- Conclusion:
- Inter-Thread Communication Example Without Synchronization
- Example 1: Main Thread Accessing Child's total Before It Is Updated
- Example 2: Main Thread Sleeping for 2 Seconds Before Accessing total
- Example 3: Using join() Method to Wait for Child Thread
- Example 4: Using activeCount() Method to Check Number of Active Threads
- Conclusion:
- Key Concepts:
- Example 1: Main Thread Sleeping and Deadlock (Infinite Wait)
- Example 2: Main Thread Waiting for 1 Second
- Example 3: Child Thread Sleeping for 3 Seconds, Main Thread Waiting for 1 Second
- Key Takeaways:
- Producer-Consumer Problem with Synchronization
- Code Example for Producer-Consumer Problem
- Differences Between notify() and notifyAll()
- IllegalMonitorStateException
- Summary
- Other Series:
In today's world of multi-core processors and highly concurrent applications, managing threads efficiently has become a key part of modern programming. When multiple threads access shared resources simultaneously, synchronization is crucial to avoid issues such as race conditions, deadlocks, and data inconsistency.
In Java, synchronization mechanisms, such as the synchronized
keyword, wait()
, notify()
, and notifyAll()
, play a vital role in managing concurrent thread execution. These tools allow us to control how threads communicate, coordinate, and access shared resources safely.
This chapter explores the concept of synchronization in Java in detail, focusing on the practical aspects of synchronized blocks and thread communication. We'll look at key examples, such as:
How synchronized blocks are used to control access to shared resources.
Identifying which thread is executing outside or inside a synchronized block.
Understanding the nuances between the
notify()
andnotifyAll()
methods in thread communication.Real-world synchronization scenarios, like the Producer-Consumer problem, and how they can be solved in Java using synchronization.
The goal of this chapter is to provide you with a clear understanding of how to use synchronization effectively to ensure thread safety, proper coordination, and avoid common concurrency pitfalls.
Section 1: Synchronized Block Examples
In Java, when multiple threads are working concurrently, there can be situations where threads need to access shared resources or data. To ensure that these resources are used safely and consistently, synchronization is used to control access to these shared resources. One of the key ways to achieve synchronization is through the use of synchronized blocks. A synchronized block ensures that only one thread can access a particular section of code at a time, preventing conflicts and data inconsistencies.
When a thread enters a synchronized block, it acquires a lock on the object specified in the synchronized statement. The lock prevents other threads from entering the synchronized block until the current thread has finished its execution. This mechanism is crucial for ensuring thread safety when multiple threads attempt to modify shared data simultaneously.
Working of Synchronized Blocks:
In a synchronized block, the lock is applied to a particular object, and the thread that holds the lock can execute the synchronized block of code. Other threads attempting to enter the synchronized block must wait until the lock is released. This mechanism ensures that the code within the block is executed by only one thread at a time.
The general syntax for a synchronized block in Java is:
synchronized (object) {
// critical section code
}
Here, object
is the reference to the object on which the lock is applied. When a thread enters the synchronized block, it locks the object. Once the thread exits the block, the lock is released, and other threads can enter the block.
Example 1: Synchronized Block in Action
Let’s consider an example where we use synchronized blocks to ensure that a method is accessed by one thread at a time:
class Display {
public void wish(String name) {
// Code before synchronized block
System.out.print("Starting to wish: ");
// Writing task in synchronized block
// Here, lock is applied for the thread executing this block
synchronized (this) {
for (int i = 1; i <= 5; i++) {
System.out.print("Good Morning: ");
try {
// Simulating a delay
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
}
}
// Code after synchronized block
System.out.println("Wishing completed for: " + name);
}
}
class MyThread extends Thread {
Display d; // Has-a relationship
String name;
MyThread(Display d, String name) {
this.d = d;
this.name = name;
}
@Override
public void run() {
d.wish(name);
}
}
public class Multithreading63 {
public static void main(String[] args) {
// Creating Display object
Display d = new Display();
// Creating MyThread objects and passing arguments
MyThread t1 = new MyThread(d, "Rohit");
MyThread t2 = new MyThread(d, "Sachin");
t1.start();
t2.start();
}
}
Explanation of the Example:
In this example, we have a class Display
that contains a method wish()
. This method prints "Good Morning" followed by the name provided as an argument. The method is synchronized using synchronized(this)
to ensure that the wish()
method is accessed by only one thread at a time.
Code Before Synchronized Block: Before entering the synchronized block, the program prints "Starting to wish". This part of the code is executed by both threads without any synchronization, so they can run concurrently without causing issues.
Synchronized Block: The synchronized block ensures that only one thread can execute the code inside it at any given time. In this case, the code within the loop (which prints "Good Morning: name") is critical, and only one thread can access it at a time. The
synchronized(this)
statement means that the thread will lock theDisplay
object to ensure that the critical section is executed by just one thread.Code After Synchronized Block: After the synchronized block, the program prints "Wishing completed for: " followed by the name. This section of the code is not synchronized, so it can be accessed by any thread.
Thread Execution:
Thread
t1
is started with the name "Ashish", and threadt2
is started with the name "Sachin".Both threads will attempt to execute the
wish()
method concurrently, but due to the synchronized block, only one thread can execute the block at a time.Thread
t1
will acquire the lock first and execute the synchronized block. Once it finishes, threadt2
will acquire the lock and execute its synchronized block.The output will show that "Good Morning: Ashish" is printed five times by thread
t1
, and only after threadt1
finishes, threadt2
will start printing "Good Morning: Sachin".
Output:
Starting to wish: Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Wishing completed for: Rohit
Starting to wish: Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Wishing completed for: Sachin
Conclusion:
In this example, we see that the synchronized block effectively prevents multiple threads from executing the critical section (the loop where the greeting is printed) simultaneously. The thread that acquires the lock of the Display
object executes the synchronized block, while the other threads must wait their turn. This ensures that the shared resource (the wish()
method) is accessed in a thread-safe manner.
Synchronized blocks are a powerful tool in multithreading, providing a means of controlling access to shared resources and preventing data corruption due to simultaneous access by multiple threads.
Example-2: Knowing Which Thread is Executing Outside the Synchronized Block
In this example, we will explore how to display which thread is executing a certain part of the code, both inside and outside of the synchronized block.
Purpose:
The goal is to show that the code before the synchronized block can be executed by multiple threads, and both threads are getting a chance to execute. In the synchronized block, however, only one thread will have the lock at any given time, and thus only one thread can execute the block.Key Concept:
TheThread.currentThread().getName()
method helps identify the current thread executing a specific part of the code. By using this, we can clearly distinguish which thread is executing before and after entering the synchronized block.
Code Explanation:
class Display {
public void wish(String name) {
// Code before synchronized block
System.out.println("Thread which is getting lock is: " + Thread.currentThread().getName()); // Line-1
// Writing task in synchronized block
// Here, lock is applied for the thread executing this block
synchronized (this) {
for (int i = 1; i <= 5; i++) {
System.out.print("Good Morning: ");
try {
// Simulating a delay
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
}
}
}
}
class MyThread extends Thread {
Display d; // Has-a relationship
String name;
MyThread(Display d, String name) {
this.d = d;
this.name = name;
}
@Override
public void run() {
d.wish(name);
}
}
public class Multithreading64 {
public static void main(String[] args) {
// Creating Display object
Display d = new Display();
// Creating MyThread objects and passing arguments
MyThread t1 = new MyThread(d, "Rohit");
MyThread t2 = new MyThread(d, "Sachin");
// Setting thread names for better identification
t1.setName("Rohit Thread");
t2.setName("Sachin");
t1.start();
t2.start();
}
}
Detailed Breakdown:
Line-1:
Before entering the synchronized block, we print the name of the thread that is executing the code usingThread.currentThread().getName()
. Since this line is outside the synchronized block, both threads (t1
andt2
) will get a chance to execute it.Synchronized Block:
Inside the synchronized block, we ensure that only one thread can execute the code at a time. The thread that acquires the lock of theDisplay
object executes the block, and others must wait for the lock to be released. This ensures thread safety while modifying shared resources or executing critical code.Thread Naming:
We explicitly set the names of the threads usingsetName()
to easily identify them in the output.Thread Execution:
Both threads (t1
andt2
) are started with the names "Ashish" and "Sachin", respectively. They both call thewish()
method of theDisplay
object, but because of the synchronization, only one thread will execute the synchronized block at a time.
Expected Output:
Thread which is getting lock is: Sachin
Thread which is getting lock is: Rohit Thread
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Rohit Thread
Good Morning: Rohit Thread
Good Morning: Rohit Thread
Good Morning: Rohit Thread
Good Morning: Rohit Thread
Explanation of Output:
Thread Execution Before the Synchronized Block:
When the threads start, both
t1
("Rohit Thread") andt2
("Sachin") print which thread is executing the code.Since
System.out.println("Thread which is getting lock is: " + Thread.currentThread().getName());
is outside the synchronized block, both threads get a chance to print their respective names. In this case,Sachin
prints first, followed byAshish Thread
.
Thread Execution Inside the Synchronized Block:
After printing the thread name, both threads attempt to enter the synchronized block to execute the greeting loop.
However, only one thread can acquire the lock at a time. Once one thread (e.g.,
Sachin
) acquires the lock and enters the synchronized block, it prints "Good Morning: Sachin" five times, and only after it finishes, the other thread (e.g.,Ashish Thread
) acquires the lock and prints "Good Morning: Ashish Thread" five times.
Conclusion:
In this example, we observed how the synchronization mechanism works in Java, especially when multiple threads are trying to access a critical section of code. The Thread.currentThread().getName()
method helped identify which thread was executing at each point in time. The code before the synchronized block was accessible to both threads, while the code within the synchronized block was protected and executed by one thread at a time. This demonstrates the fundamental behavior of synchronization in managing concurrent access to shared resources in multithreaded environments.
Example-3: Knowing Which Thread is Getting Lock and Which Thread is Releasing Lock
In this example, we explore how to display which thread acquires the lock and which thread releases the lock when entering and exiting a synchronized block.
Purpose:
The goal is to observe which thread gets the lock for the synchronized block and which thread releases it after completing the execution. The synchronized block ensures that only one thread can execute it at a time, and it demonstrates thread behavior in acquiring and releasing locks.Key Concept:
TheThread.currentThread().getName()
method is used to print the name of the thread that is getting and releasing the lock. Since only one thread can hold the lock at a time inside the synchronized block, it will be easier to see how the lock is passed from one thread to another.
Code Explanation:
class Display {
public void wish(String name) {
// Code before synchronized block
// Writing task in synchronized block
// Here, lock is applied for the thread executing this block
synchronized (this) {
// Knowing thread which is getting lock
System.out.println("Thread which is getting lock is: " + Thread.currentThread().getName()); // Line-1
for (int i = 1; i <= 5; i++) {
System.out.print("Good Morning: ");
try {
// Simulating a delay
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
}
}
// Knowing thread which is releasing lock
System.out.println("Thread which is releasing lock is: " + Thread.currentThread().getName()); // Line-2
}
}
class MyThread extends Thread {
Display d; // Has-a relationship
String name;
MyThread(Display d, String name) {
this.d = d;
this.name = name;
}
@Override
public void run() {
d.wish(name);
}
}
public class Multithreading65 {
public static void main(String[] args) {
// Creating Display object
Display d = new Display();
// Creating MyThread objects and passing arguments
MyThread t1 = new MyThread(d, "Rohit");
MyThread t2 = new MyThread(d, "Sachin");
// Setting thread names for better identification
t1.setName("Rohit Thread");
t2.setName("Sachin Thread");
t1.start();
t2.start();
}
}
Detailed Breakdown:
Line-1 (Inside the synchronized block):
- As the threads enter the synchronized block, they print which thread is acquiring the lock by calling
Thread.currentThread().getName()
. Only one thread can acquire the lock at a time and enter the synchronized block.
- As the threads enter the synchronized block, they print which thread is acquiring the lock by calling
Synchronized Block:
- The thread that acquires the lock executes the loop and prints "Good Morning: " five times, where
<name>
is either "Rohit" or "Sachin". Since the synchronized block is guarded by a lock, only one thread can access the block at any given moment.
- The thread that acquires the lock executes the loop and prints "Good Morning: " five times, where
Line-2 (After exiting the synchronized block):
- Once the thread completes its execution inside the synchronized block, it releases the lock. The thread then prints which thread is releasing the lock. This happens after the synchronized block finishes execution.
Thread Naming:
- We explicitly set the names of the threads using
setName()
to make it easy to identify in the output. This allows us to clearly see which thread is executing the critical section and when it is releasing the lock.
- We explicitly set the names of the threads using
Expected Output:
Thread which is getting lock is: Rohit Thread
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Thread which is releasing lock is: Rohit Thread
Thread which is getting lock is: Sachin Thread
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Thread which is releasing lock is: Sachin Thread
Explanation of Output:
Thread Execution Before the Synchronized Block:
- The threads start execution, and
Rohit Thread
is the first to acquire the lock and enter the synchronized block. The thread prints the message "Thread which is getting lock is: Rohit Thread" and then executes the loop, printing "Good Morning: Rohit" five times.
- The threads start execution, and
Thread Execution Inside the Synchronized Block:
- After printing "Good Morning: Rohit",
Rohit Thread
holds the lock and executes the synchronized block for its entire duration (5 iterations). No other thread can enter the synchronized block whileRohit Thread
is executing inside it.
- After printing "Good Morning: Rohit",
Releasing the Lock:
Once
Rohit Thread
finishes executing the loop inside the synchronized block, it releases the lock and prints "Thread which is releasing lock is: Rohit Thread".Then,
Sachin Thread
acquires the lock and enters the synchronized block. It prints the same messages, showing that only one thread can execute inside the synchronized block at a time.
Conclusion:
In this example, we clearly observe the behavior of synchronized blocks in Java. The Thread.currentThread().getName()
method allows us to identify which thread is getting the lock and which is releasing it. The synchronized block ensures that only one thread can execute it at a time, providing thread safety for shared resources. This pattern is useful when managing critical sections of code where access needs to be synchronized across multiple threads.
Example-4: Multiple Threads Operating on Multiple Objects
In this example, we demonstrate the behavior of multiple threads operating on multiple objects. The threads will execute tasks on two different Display
objects (d1
and d2
), and we will notice irregular output because each thread locks its own object, allowing them to run concurrently.
Key Concept:
In this example, each thread operates on its own object. Since the synchronized block usessynchronized(this)
, it locks the instance (this
) of the object the thread is currently working on. This means that two threads can operate concurrently on different objects, but not on the same object at the same time.Observation:
As the threads operate on differentDisplay
objects (d1
andd2
), we will see irregular output. The threads will print messages interspersed due to them operating on different objects simultaneously.
Code Explanation:
class Display {
public void wish(String name) {
// Code before synchronized block
// Writing task in synchronized block
// Here, lock is applied for the thread executing this block
synchronized(this) {
// Knowing thread which is getting lock
System.out.println("Thread which is getting lock is: " + Thread.currentThread().getName()); // Line-1
for (int i = 1; i <= 5; i++) {
System.out.print("Good Morning: ");
try {
// Simulating a delay
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
}
}
// Knowing thread which is releasing lock
System.out.println("Thread which is releasing lock is: " + Thread.currentThread().getName()); // Line-2
}
}
class MyThread extends Thread {
Display d; // Has-a relationship
String name;
MyThread(Display d, String name) {
this.d = d;
this.name = name;
}
@Override
public void run() {
d.wish(name);
}
}
public class Multithreading66 {
public static void main(String[] args) {
// Creating multiple display objects
Display d1 = new Display();
Display d2 = new Display();
// Creating MyThread objects and passing arguments
MyThread t1 = new MyThread(d1, "Rohit");
MyThread t2 = new MyThread(d2, "Sachin");
// Setting thread names for better identification
t1.setName("Rohit Thread");
t2.setName("Sachin Thread");
// Starting the threads
t1.start();
t2.start();
}
}
Detailed Breakdown:
Line-1 (Inside the synchronized block):
- Each thread prints which thread is acquiring the lock using
Thread.currentThread().getName()
. The lock is applied to thethis
object, which is eitherd1
ord2
, depending on the thread's execution. Since there are two differentDisplay
objects (d1
andd2
), two threads can acquire locks simultaneously but on separate objects.
- Each thread prints which thread is acquiring the lock using
Synchronized Block:
- The thread that acquires the lock executes the loop, printing "Good Morning: " five times, where
<name>
is the argument passed to thewish()
method.
- The thread that acquires the lock executes the loop, printing "Good Morning: " five times, where
Line-2 (After exiting the synchronized block):
- Once the thread completes execution inside the synchronized block, it releases the lock. After releasing the lock, the thread prints which thread is releasing the lock.
Thread Naming:
- The threads are given specific names using
setName()
for easier identification in the output. This ensures that we can distinguish between the two threads and observe how they behave concurrently.
- The threads are given specific names using
Expected Output:
Thread which is getting lock is: Rohit Thread
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Rohit
Good Morning: Sachin
Good Morning: Rohit
Good Morning: Sachin
Good Morning: Rohit
Good Morning: Sachin
Thread which is releasing lock is: Rohit Thread
Thread which is releasing lock is: Sachin Thread
Explanation of Output:
Thread Execution Before the Synchronized Block:
- The threads start execution, and the
Rohit Thread
acquires the lock for thed1
object and enters the synchronized block. It prints "Thread which is getting lock is: Rohit Thread" and starts executing the loop, printing "Good Morning: Rohit" five times.
- The threads start execution, and the
Thread Execution Inside the Synchronized Block:
- The
Rohit Thread
holds the lock for thed1
object, while theSachin Thread
can execute its synchronized block on thed2
object at the same time. The threads print their respective greetings ("Good Morning: Rohit" and "Good Morning: Sachin") concurrently.
- The
Releasing the Lock:
- Once the
Rohit Thread
finishes its execution inside the synchronized block, it releases the lock ond1
, and theSachin Thread
releases the lock ond2
after completing its own synchronized block.
- Once the
Irregular Output:
- Since each thread operates on a different object (
d1
andd2
), they can execute concurrently. This results in the output appearing irregular, with interspersed prints for "Good Morning: Rohit" and "Good Morning: Sachin". Both threads acquire and release their respective locks independently.
- Since each thread operates on a different object (
Conclusion:
In this example, the threads are operating on two different objects (d1
and d2
), which allows them to execute concurrently without blocking each other. The use of synchronized(this)
ensures that only one thread can access the synchronized block for each object at a time. This demonstrates how multiple threads can operate on multiple objects simultaneously, leading to irregular output when both threads are running concurrently.
Example-5: Class-Level Lock Applied
In this example, we demonstrate how a class-level lock works in Java using the synchronized
keyword. This type of lock is applied at the class level, meaning that when one thread acquires the lock on the class, no other thread can acquire the same lock on that class, even if the thread is operating on a different object.
Key Concept:
The lock applied here is at the class level (i.e.,
Display.class
). This means that while one thread has acquired the lock for a class, no other thread can acquire the same lock, regardless of the object being used.The lock applies to the class itself, not to the instance of the class. This behavior ensures mutual exclusion for threads attempting to access synchronized blocks on the same class, making the output regular.
Observation:
- In this case, as two different threads (
Ashish Thread
andSachin Thread
) operate on two separate objects (d1
andd2
), they will still contend for the class-level lock, leading to regular output. Each thread can access the synchronized block only when it acquires the class-level lock.
- In this case, as two different threads (
Code Explanation:
class Display {
public void wish(String name) {
// Code before synchronized block
// Writing task in synchronized block
// Here, class-level lock is applied using Display.class
synchronized (Display.class) {
// Knowing thread which is getting lock
System.out.println("Thread which is getting lock is: " + Thread.currentThread().getName()); // Line-1
for (int i = 1; i <= 5; i++) {
System.out.print("Good Morning: ");
try {
// Simulating a delay
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
}
}
// Knowing thread which is releasing lock
System.out.println("Thread which is releasing lock is: " + Thread.currentThread().getName()); // Line-2
}
}
class MyThread extends Thread {
Display d; // Has-a relationship
String name;
MyThread(Display d, String name) {
this.d = d;
this.name = name;
}
@Override
public void run() {
d.wish(name);
}
}
public class Multithreading67 {
public static void main(String[] args) {
// Creating multiple display objects
Display d1 = new Display();
Display d2 = new Display();
// Creating MyThread objects and passing arguments
MyThread t1 = new MyThread(d1, "Rohit");
MyThread t2 = new MyThread(d2, "Sachin");
// Setting thread names for better identification
t1.setName("Rohit Thread");
t2.setName("Sachin Thread");
// Starting the threads
t1.start();
t2.start();
}
}
Detailed Breakdown:
Class-Level Lock:
The line
synchronized (Display.class)
applies a class-level lock. It ensures that only one thread can execute the synchronized block at a time, even if there are multiple instances of theDisplay
class.When one thread acquires the lock on the
Display.class
, other threads cannot enter any synchronized block that uses the same lock.
Thread Execution Inside the Synchronized Block:
- The threads are executed with a synchronized block on
Display.class
. The first thread to acquire the lock will print "Good Morning" five times, and only after it finishes will the second thread get a chance to execute.
- The threads are executed with a synchronized block on
Releasing the Lock:
- Once the thread completes its execution inside the synchronized block, it releases the lock on
Display.class
. This allows the next thread to acquire the lock and execute its block.
- Once the thread completes its execution inside the synchronized block, it releases the lock on
Thread Naming:
- The threads are named
Rohit Thread
andSachin Thread
for easier identification in the output. This ensures that we can clearly observe which thread is executing and when it acquires and releases the lock.
- The threads are named
Expected Output:
Thread which is getting lock is: Rohit Thread
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Good Morning: Rohit
Thread which is releasing lock is: Rohit Thread
Thread which is getting lock is: Sachin Thread
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Thread which is releasing lock is: Sachin Thread
Explanation of Output:
Thread Execution Before the Synchronized Block:
- The
Rohit Thread
acquires the class-level lock forDisplay.class
. It prints "Thread which is getting lock is: Rohit Thread" and executes the synchronized block. It prints "Good Morning: Rohit" five times.
- The
Thread Execution Inside the Synchronized Block:
- Since the lock is applied at the class level, even though the threads are working on different
Display
objects (d1
andd2
), the second thread (Sachin Thread
) must wait for theRohit Thread
to finish its synchronized block before it can acquire the lock and proceed.
- Since the lock is applied at the class level, even though the threads are working on different
Releasing the Lock:
- Once the
Rohit Thread
finishes its execution inside the synchronized block, it releases the lock onDisplay.class
, allowing theSachin Thread
to acquire the lock. TheSachin Thread
then starts executing its synchronized block and prints "Good Morning: Sachin" five times.
- Once the
Regular Output:
- The output is regular because the class-level lock ensures that only one thread can access the synchronized block at any given time, even though the threads are operating on different
Display
objects.
- The output is regular because the class-level lock ensures that only one thread can access the synchronized block at any given time, even though the threads are operating on different
Conclusion:
In this example, we see how a class-level lock (synchronized(Display.class)
) works. The key observation is that even though threads are operating on different objects, they must acquire the same class-level lock before executing the synchronized block. This results in a regular output where one thread completes its execution before the next thread starts, ensuring mutual exclusion at the class level. This demonstrates the concept of synchronization at the class level in multithreading.
2. Scenario to Understand Inter-Thread Communication
Example-5: Person-1, Person-2, and Postman Example
In this example, we explore the concept of inter-thread communication through a scenario involving two people and a postman.
Scenario Breakdown:
Two People and a Postman:
Person-2 tells Person-1 that they will send an important letter via the postman.
Person-1, eagerly waiting for the letter, checks the postbox multiple times between 5:10 PM and 10:00 AM. However, the letter hasn’t arrived yet.
At 1:00 PM, the postman drops the letter in the postbox.
Person-1 checks the postbox, finds the letter, and discovers that Person-2 is out of station and will call back later.
Inter-thread Communication Analogy:
Person-1 (Thread-1) is constantly checking the postbox, even though the letter hasn’t arrived yet. This leads to inefficient resource usage, as Person-1 keeps checking without any new information.
Person-2 (Thread-2), the postman, is the one responsible for delivering the letter.
The resource (the letter in the postbox) isn’t available immediately for Person-1, but Person-1 keeps checking unnecessarily, wasting time and resources.
Performance Issue:
- This scenario highlights inefficiency. Thread-1 (Person-1) keeps checking for the resource instead of performing other useful tasks, leading to a performance issue where the thread is stuck waiting instead of being productive.
Solution:
A solution for this scenario is for Person-1 to wait for a notification from the postman (Person-2/Thread-2) rather than constantly checking the postbox.
Instead of checking repeatedly, Person-1 should perform other tasks and only check the postbox when notified by the postman.
This communication between threads (Person-1 and Postman) is facilitated by Inter-Thread Communication.
What Needs to Happen?
Thread-1 (Person-1) should be notified by Thread-2 (Postman) when the letter is in the postbox.
Thread-1 (Person-1) can perform its own tasks while waiting for the notification and then proceed to check the postbox once notified.
Thread-2 (Postman) will notify Thread-1 (Person-1) when the letter is placed in the postbox.
This mechanism ensures that Thread-1 (Person-1) isn’t constantly checking for the resource (the letter), thus improving efficiency.
This approach involves using the wait()
, notify()
, and notifyAll()
methods in Java, which allow threads to communicate and coordinate their actions effectively.
In Java:
We can implement this scenario using inter-thread communication where:
Thread-1 (Person-1) waits on the postbox object until the postman places the letter in the postbox.
Thread-2 (Postman) notifies Thread-1 (Person-1) when the letter is available.
Here is how Inter-Thread Communication is implemented in Java.
Inter-Thread Communication in Java
Inter-Thread Communication (or Co-operation) is the mechanism in Java where threads can communicate with each other using the following methods of the Object
class:
wait()
- Causes the current thread to wait until another thread notifies it.notify()
- Wakes up one thread that is waiting on the object.notifyAll()
- Wakes up all threads that are waiting on the object.
These methods are used in a synchronized context, where one thread waits for a condition to be met (e.g., waiting for the letter to be in the postbox), and another thread notifies the waiting thread when the condition is satisfied (e.g., when the postman drops the letter in the postbox).
Key Concepts:
wait()
: A thread that callswait()
on an object enters a waiting state until another thread callsnotify()
ornotifyAll()
on that same object.notify()
: Wakes up one thread that is waiting on the object.notifyAll()
: Wakes up all threads that are waiting on the object.
These methods allow threads to cooperate by notifying each other when they can proceed, ensuring that they are not wasting resources by continuously checking for a resource that may not be available.
Example Code for Inter-Thread Communication:
class Postbox {
private boolean isLetterAvailable = false; // Flag to check if letter is available
// Method to simulate the Postman placing a letter
public synchronized void deliverLetter() {
System.out.println("Postman is delivering the letter...");
try {
Thread.sleep(2000); // Simulate time taken by the postman to deliver the letter
} catch (InterruptedException e) {
e.printStackTrace();
}
isLetterAvailable = true;
notify(); // Notify Person-1 that the letter is available
}
// Method to simulate Person-1 waiting for the letter
public synchronized void checkLetter() {
while (!isLetterAvailable) {
try {
System.out.println("Person-1 is waiting for the letter...");
wait(); // Wait until Postman delivers the letter
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Person-1 got the letter!");
}
}
class Person1 extends Thread {
Postbox postbox;
Person1(Postbox postbox) {
this.postbox = postbox;
}
@Override
public void run() {
postbox.checkLetter(); // Person-1 waits for the letter
}
}
class Postman extends Thread {
Postbox postbox;
Postman(Postbox postbox) {
this.postbox = postbox;
}
@Override
public void run() {
postbox.deliverLetter(); // Postman delivers the letter
}
}
public class InterThreadCommunicationExample {
public static void main(String[] args) {
Postbox postbox = new Postbox(); // Shared resource
Person1 person1 = new Person1(postbox);
Postman postman = new Postman(postbox);
person1.start(); // Start Person-1 thread
postman.start(); // Start Postman thread
}
}
Explanation of Code:
The
Postbox
class acts as the shared resource. It has a flag (isLetterAvailable
) that indicates whether the letter is in the postbox.Person-1 (
Person1
thread) checks if the letter is available. If not, it callswait()
to enter a waiting state.The Postman (
Postman
thread) simulates delivering the letter by setting theisLetterAvailable
flag totrue
and then callingnotify()
to wake up Person-1.Person-1 wakes up when notified, checks the postbox, and receives the letter.
Expected Output:
Person-1 is waiting for the letter...
Postman is delivering the letter...
Person-1 got the letter!
Conclusion:
This example demonstrates how inter-thread communication works in Java. By using the wait()
, notify()
, and notifyAll()
methods, threads can communicate with each other, ensuring that they only proceed when the necessary conditions are met. This approach avoids unnecessary busy-waiting and improves performance by allowing threads to cooperate effectively.
Understanding wait()
, notify()
, and notifyAll()
Methods in Java
Inter-thread communication in Java allows threads to coordinate their actions, enabling them to share resources and information effectively. This communication is facilitated by three key methods:
wait()
notify()
notifyAll()
Let’s dive deeper into the wait()
method first, and then explore how it interacts with the notify()
and notifyAll()
methods.
1. wait()
Method
Purpose of wait()
Method:
The
wait()
method is used when a thread is waiting for a condition to be met before it can proceed with its execution.It is typically used in scenarios where Thread-1 (e.g., Person-1) is waiting for a resource that Thread-2 (e.g., the Postman) will provide.
Key Points about wait()
Method:
Thread Waiting for Notification:
A thread that is waiting for a notification or update should call the
wait()
method.This makes the calling thread enter a waiting state and effectively pauses its execution.
Who Should Call
wait()
?The thread that is expecting a resource or information from another thread should invoke
wait()
.For example, in our analogy, Person-1 (Thread-1) should call
wait()
because they are waiting for the postman (Thread-2) to deliver the letter.
Thread Ownership of the Monitor:
The thread calling
wait()
must own the object's monitor. In simpler terms, it must be inside a synchronized block or method. If not, it will throw anIllegalMonitorStateException
.This ensures that only the thread that has the lock on the object can wait for the condition to be satisfied.
Releasing the Lock:
When
wait()
is called, the current thread releases the lock on the object it holds the monitor for.This is important because, when the thread is waiting, other threads can enter the synchronized block and perform necessary updates or actions (like Thread-2 delivering the letter).
Resuming After Notification:
The thread that calls
wait()
will stay in the waiting state until one of the following happens:Another thread calls
notify()
ornotifyAll()
, which signals the waiting thread that it can proceed.Alternatively, if a timeout is specified, the thread may be awakened after the specified time.
Works Only in Synchronized Context:
All these methods (
wait()
,notify()
,notifyAll()
) work only within synchronized blocks or methods.This ensures thread safety when multiple threads are interacting with shared resources.
Illustrative Example of wait()
Method:
class Postbox {
private boolean isLetterAvailable = false;
// Method for Person-1 to wait for the letter
public synchronized void checkLetter() {
while (!isLetterAvailable) {
try {
System.out.println("Person-1 is waiting for the letter...");
wait(); // Person-1 waits for the letter to be delivered
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Person-1 got the letter!");
}
// Method for Postman to deliver the letter
public synchronized void deliverLetter() {
try {
System.out.println("Postman is delivering the letter...");
Thread.sleep(2000); // Simulating delivery time
} catch (InterruptedException e) {
e.printStackTrace();
}
isLetterAvailable = true;
notify(); // Notify Person-1 that the letter is available
}
}
Explanation of Code:
checkLetter()
method: Person-1 (Thread-1) is waiting for the letter, so it callswait()
. This means Person-1 will stop executing until Postman (Thread-2) delivers the letter.deliverLetter()
method: Postman (Thread-2) delivers the letter and then callsnotify()
to signal Person-1 that the letter is now available.
2. notify()
Method
Purpose of notify()
Method:
The
notify()
method is used to wake up one of the threads that are waiting on the same object.Once the condition is met (e.g., letter is delivered), the thread that called
notify()
signals the waiting thread to resume execution.
Key Points about notify()
Method:
Wakes Up One Thread:
The
notify()
method wakes up one thread that is currently waiting on the same object. If there are multiple threads waiting, only one thread is chosen.This thread will then attempt to re-acquire the monitor (lock) and proceed with its execution.
Should be Called After
wait()
:notify()
should be invoked after a thread (e.g., Postman) has finished updating the shared resource (e.g., delivered the letter).
3. notifyAll()
Method
Purpose of notifyAll()
Method:
The
notifyAll()
method is similar tonotify()
, but it wakes up all waiting threads on the same object, not just one.It is useful when there are multiple threads waiting for a condition, and you want all of them to resume their execution when the condition is met.
Key Points about notifyAll()
Method:
Wakes Up All Waiting Threads:
Unlike
notify()
, which wakes up only one thread,notifyAll()
wakes up all threads waiting on the object.Each of the threads will re-acquire the lock and proceed when the condition is satisfied.
Use Case:
notifyAll()
is useful when multiple threads are waiting for different conditions or need to be notified simultaneously.
Summary of Methods:
wait()
:Causes the current thread to wait until another thread signals it by calling
notify()
ornotifyAll()
.Can only be called from within synchronized methods/blocks.
Releases the lock, allowing other threads to acquire it.
notify()
:Wakes up one thread that is waiting on the object.
Typically called after updating the shared resource (e.g., after the postman delivers the letter).
notifyAll()
:Wakes up all threads that are waiting on the object.
Useful when multiple threads are waiting for different conditions or need to be notified at the same time.
These methods allow threads to cooperate and share resources effectively, improving performance and preventing inefficient busy-waiting.
Understanding notify()
, notifyAll()
, and Thread Ownership
The notify()
, notifyAll()
, and wait()
methods are fundamental in thread communication in Java. These methods help threads coordinate their actions and synchronize access to shared resources. Let's break down the key concepts related to these methods, focusing on how thread ownership and synchronization affect their behavior.
2. notify()
and notifyAll()
Methods
Purpose of notify()
and notifyAll()
:
notify()
andnotifyAll()
are used by a thread that has completed an update to a shared resource. These methods notify waiting threads that the resource or condition they were waiting for has been updated and they can proceed.
Key Points about notify()
and notifyAll()
:
Thread Performing Update:
- The thread that is responsible for updating the resource (e.g., delivering the letter, adding items to a list) should call
notify()
ornotifyAll()
. This is to inform other threads that are waiting for this resource that they can now resume execution with the updated resource.
- The thread that is responsible for updating the resource (e.g., delivering the letter, adding items to a list) should call
Thread Calling
notify()
:When a thread calls
notify()
, it sends a notification to one of the threads that are waiting on the same object. The waiting thread will resume its execution once it can acquire the lock again.Thread Lock and Release:
- The thread calling
notify()
may or may not release the lock immediately. However, the lock will eventually be released so that the notified thread can proceed.
- The thread calling
Thread Lock Ownership:
- The thread calling
wait()
,notify()
, ornotifyAll()
must be the owner of the object's monitor. This means the thread must hold the lock on the object, and these methods can only be called within synchronized methods or blocks. If a thread tries to call these methods outside of synchronized context, it will throw anIllegalMonitorStateException
.
- The thread calling
Lock Behavior:
When a thread calls
wait()
, it immediately releases the lock on the object and enters a waiting state. The thread will stay in this state until another thread callsnotify()
ornotifyAll()
, signaling it to proceed.On the other hand, when a thread calls
notify()
, it may or may not release the lock immediately. However, once thenotify()
ornotifyAll()
method completes, the lock will be released.
Thread Ownership and Synchronization Context
Thread Ownership of the Object:
A thread is considered to be the owner of the object if it holds the lock for that object. This lock is necessary to ensure that the thread has exclusive access to the shared resource.
This ownership ensures thread safety during updates and waiting processes.
Synchronized Blocks and Methods:
wait()
,notify()
, andnotifyAll()
can only be used within synchronized blocks or synchronized methods. These methods interact with the object’s monitor (lock), so they need to be executed in a synchronized context to ensure that the thread has the lock on the object.
IllegalMonitorStateException:
If a thread tries to call
wait()
,notify()
, ornotifyAll()
outside of a synchronized context, it will throw anIllegalMonitorStateException
.- This exception occurs because the thread is not the owner of the object's monitor, which is required for using these methods.
Summary of Key Concepts:
Notify Thread Updates:
- The thread that updates the shared resource should call
notify()
ornotifyAll()
to wake up waiting threads.
- The thread that updates the shared resource should call
Lock Release with
wait()
andnotify()
:wait()
causes the calling thread to release the lock immediately and enter the waiting state.notify()
may or may not release the lock immediately, but the lock will be eventually released so that other threads can proceed.
Thread Ownership:
A thread must own the object’s monitor to call
wait()
,notify()
, ornotifyAll()
.These methods must be called within synchronized blocks or methods; otherwise, an
IllegalMonitorStateException
will be thrown.
Synchronized Methods/Blocks:
- Synchronized blocks are critical for ensuring that only one thread at a time can update or access the shared resource, avoiding conflicts.
Illustrative Example:
class SharedResource {
private boolean dataAvailable = false;
// Method for Thread-A to wait for the data
public synchronized void waitForData() {
while (!dataAvailable) {
try {
System.out.println("Thread-A is waiting for data...");
wait(); // Thread-A waits for data to be available
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread-A got the data!");
}
// Method for Thread-B to provide the data and notify Thread-A
public synchronized void provideData() {
System.out.println("Thread-B is providing the data...");
dataAvailable = true;
notify(); // Notify Thread-A that data is available
}
}
Explanation of Code:
Thread-A calls
waitForData()
and enters the waiting state because the data is not available yet.Thread-B calls
provideData()
and updates the shared resource. After that, it callsnotify()
to wake up Thread-A so it can continue executing and use the updated data.
By properly using wait()
, notify()
, and notifyAll()
, we can synchronize threads and ensure safe communication between them, avoiding issues like race conditions and deadlocks.
Important Points on Inter-Thread Communication in Java
In multithreaded programming, threads need to communicate with each other to coordinate their actions. This is achieved through inter-thread communication mechanisms in Java, such as wait()
, notify()
, and notifyAll()
, which are methods in the Object
class. There are also other methods, such as yield()
, sleep()
, and join()
, which are part of the Thread
class. Let's explore the important points and distinctions related to these methods.
Thread Class Methods: yield()
, sleep()
, join()
yield()
:It is a static method of the
Thread
class.Purpose: It tells the current thread to yield its current time slice, allowing other threads of the same priority to run.
Effect: It does not release any locks. The thread will continue executing after the other thread(s) run, but the CPU time is shared.
sleep(long millis)
:It is a static method of the
Thread
class.Purpose: It pauses the current thread for a specified number of milliseconds (and optionally nanoseconds). It does not release locks but merely suspends the thread's execution.
Effect: The thread remains in the sleeping state for the specified time but holds onto any locks it currently has.
join()
:It is a method of the
Thread
class.Purpose: It makes the current thread wait until the thread it is called on has finished executing.
Effect: It does not release locks on shared resources, but the calling thread is blocked until the target thread completes.
Object Class Methods: wait()
, notify()
, notifyAll()
wait()
:Prototype:
public final void wait() throws InterruptedException
Purpose: Causes the current thread to wait until it is notified by another thread or a specified amount of time has passed.
Effect: It releases the lock on the object it is called on and enters a waiting state. Once notified, it re-acquires the lock and resumes execution.
wait(long ms)
:Prototype:
public final native void wait(long ms) throws InterruptedException
Purpose: Similar to
wait()
, but it allows the thread to wait for a specific time (in milliseconds).Effect: The thread waits for the specified time unless it is notified earlier.
wait(long ms, int ns)
:Prototype:
public final void wait(long ms, int ns) throws InterruptedException
Purpose: This method allows the thread to wait for the specified time, but with more precise control (milliseconds and nanoseconds).
Effect: Similar to
wait(long ms)
but with finer granularity.
notify()
:Prototype:
public final native void notify()
Purpose: Wakes up one thread that is waiting on the object.
Effect: The thread calling this method releases the lock on the object, and the waiting thread is notified to resume execution.
notifyAll()
:Prototype:
public final void notifyAll()
Purpose: Wakes up all threads that are waiting on the object.
Effect: All waiting threads are notified and will compete for the lock on the object.
Key Differences Between yield()
, sleep()
, join()
, and wait()
, notify()
, notifyAll()
Lock Behavior:
yield()
,sleep()
,join()
: These methods do not release the lock. The thread continues holding onto the lock during their execution, even though the thread may be paused or waiting.wait()
,notify()
,notifyAll()
: These methods release the lock on the object and allow other threads to acquire it and continue execution. Without releasing the lock, inter-thread communication cannot occur.
Purpose:
yield()
: Used for cooperative multitasking, allowing other threads of the same priority to execute.sleep()
: Pauses the current thread for a specified time.join()
: Makes the current thread wait for another thread to finish.wait()
,notify()
,notifyAll()
: Used for inter-thread communication to synchronize threads and coordinate their actions based on shared resources.
Why Are wait()
, notify()
, notifyAll()
in the Object
Class, Not Thread
Class?
Inter-thread Communication: These methods are used to communicate between threads, and they operate on shared resources, which can be any object in Java. The thread may need to communicate over a shared resource, like a PostBox, Stack, or Customer, etc.
Availability for All Objects: Since every object in Java is an instance of the
Object
class, these methods must be available for every object. Therefore, they are part of theObject
class, not theThread
class.Objects as Shared Resources: Thread communication happens between threads operating on shared objects. If these methods were part of the
Thread
class, only threads would have access to them, making inter-thread communication on shared objects impossible. Thus, placing them in theObject
class ensures that any object can be used for synchronization and communication between threads.
Summary of Key Concepts:
yield()
,sleep()
,join()
: These methods belong to theThread
class and deal with thread execution behavior. They do not release the lock during their execution.wait()
,notify()
,notifyAll()
: These methods belong to theObject
class and are used for inter-thread communication. They release the lock to allow other threads to access shared resources and synchronize their actions.Thread Communication:
wait()
,notify()
, andnotifyAll()
are placed in theObject
class because communication often involves shared objects, and every object in Java is derived from theObject
class. Therefore, these methods need to be accessible from any object in Java.
This provides an overview of the key differences and reasons why certain methods are located in different classes in Java.
Thread Life Cycle When a Thread Calls wait()
Method
The life cycle of a thread can be impacted when a thread calls the wait()
method. Below is an explanation of the thread's journey through different states and how it behaves when interacting with synchronization mechanisms like wait()
.
Thread Life Cycle States:
New/Born State:
This is the state when a thread is created but hasn't yet started.
The thread has been instantiated, but
start()
has not been called.
Ready/Runnable State:
After the thread calls
start()
, it enters the Ready/Runnable state, where it is eligible for CPU time.The operating system’s scheduler will allocate time for the thread to execute when resources are available.
Running State:
The thread enters the Running state when the CPU allocates it time to execute. The thread is currently executing its
run()
method.When a thread is running, it may call synchronization methods like
wait()
,notify()
, ornotifyAll()
.
Waiting State:
If the thread calls
wait()
, it enters the Waiting State.The thread will stay in this state until it either receives a notification (via
notify()
ornotifyAll()
), or it is interrupted, or a specified time expires ifwait(long ms)
orwait(long ms, int ns)
is used.During this state, the thread does not hold the lock and will wait for the lock to be released by another thread.
Blocked/Waiting for Lock:
After calling
wait()
, the thread enters another waiting state, where it is waiting for the lock to be released.This state occurs if there are other threads with the lock, and the waiting thread will remain in this state until it successfully acquires the lock.
Runnable/Ready State (After Wait):
Once the thread has been notified and successfully re-acquires the lock, it enters the Runnable/Ready state.
From here, it can be scheduled for execution again.
Dead State:
- A thread enters the Dead state when its
run()
method has completed execution, or it has been terminated (either through normal termination or via an exception).
- A thread enters the Dead state when its
Important Transitions from Waiting State:
A thread can come out of the Waiting State under the following conditions:
When the Thread Gets the Lock or Receives Notification:
- When the thread receives a notification (via
notify()
ornotifyAll()
), it is awakened from the waiting state and enters the Runnable/Ready state. The thread must first re-acquire the lock to resume execution.
- When the thread receives a notification (via
When Time Expiry Occurs:
If the thread is using
wait(long ms)
orwait(long ms, int ns)
to wait for a specified amount of time, once the time expires, the thread will come out of the waiting state automatically.It will then re-enter the Runnable/Ready state, where it will be eligible to be scheduled for execution.
When the Thread is Interrupted:
If the thread is interrupted while waiting, it will come out of the waiting state and resume execution.
The interruption can be checked with the
InterruptedException
that the thread may throw. If an interrupt occurs, the thread will handle the exception and proceed accordingly.
Summary of Thread Flow with wait()
Method:
Thread Starts: Initially, the thread is in the New/Born state. Once
start()
is called, it moves to the Ready/Runnable state and is eligible to run.Thread Executes: The thread enters the Running state and begins executing its
run()
method.Thread Calls
wait()
: If the thread callswait()
, it enters the Waiting state, where it releases the lock and waits for notification or lock acquisition.Thread Waits for Lock or Notification: The thread enters a waiting-for-lock state where it waits to re-acquire the lock or receive a notification.
Thread Wakes Up: The thread exits the Waiting state and re-enters the Runnable/Ready state:
After receiving a notification (
notify()
ornotifyAll()
).After the specified waiting time expires (if using
wait(long ms)
).If the thread is interrupted.
Thread Completes: Once the thread has finished executing its
run()
method, it enters the Dead state, indicating the thread's lifecycle is complete.
This illustrates the flow of a thread's life cycle when it calls the wait()
method and how it interacts with synchronization in Java.
Inter-Thread Communication Example Explanation
In this example, we will demonstrate the concept of inter-thread communication using the wait()
and notify()
methods in Java, with synchronization applied on shared resources.
The goal of this example is to show how a main thread and a child thread can communicate using a shared variable (total
), and how the main thread waits for the child thread to complete its task before proceeding. The child thread will calculate the sum of 100 numbers, and the main thread will wait for the result before printing it.
Key Concepts in This Example:
Synchronization:
The
synchronized(this)
block ensures that only one thread at a time can execute the critical section of code (where thetotal
variable is updated or accessed).The main thread and child thread are both trying to access the same object (
b
), and synchronization helps ensure that the threads don’t interfere with each other.
wait()
andnotify()
:wait()
is called by the main thread, which makes it enter the waiting state. It will only wake up when the child thread callsnotify()
.notify()
is called by the child thread to wake up the main thread once the calculation is completed.
Code Breakdown:
Main Thread:
The main thread starts and creates an object of
ThreadB
.It enters a synchronized block on
ThreadB
object (b
), callingwait()
to wait for a notification from the child thread.
Child Thread (
ThreadB
):The child thread calculates the sum of numbers from 1 to 100 in a synchronized block, ensuring that it has exclusive access to the
total
variable during the calculation.Once the calculation is complete, the child thread calls
notify()
to notify the main thread that the result is available.
Step-by-Step Execution:
Step 1: Main Thread Calls wait()
:
The main thread enters the synchronized block on the
b
object.It calls
b.wait()
, causing it to enter the waiting state.At this point, the main thread releases the lock on
b
and waits for the child thread to complete its calculation and notify it.
synchronized(b) {
System.out.println("Main thread calling wait() method");
b.wait(); // Main thread enters the waiting state.
System.out.println("Main thread got notification call");
System.out.println(b.total); // Prints the total calculated by the child thread.
}
Step 2: Child Thread Starts Calculation:
The child thread starts its execution by calling the
run()
method.Inside the
run()
method, the child thread enters a synchronized block (synchronized(this)
) to ensure exclusive access to thetotal
variable while performing the sum calculation.
synchronized(this) {
System.out.println("Child thread started the calculation");
for (int i = 1; i <= 100; i++) {
total += i; // Adds numbers from 1 to 100.
}
System.out.println("Child thread giving notification call");
this.notify(); // Notifies the main thread.
}
Step 3: Child Thread Sends Notification:
Once the sum is calculated, the child thread calls
notify()
on theThreadB
object (this
), waking up the main thread.The child thread releases the lock on
b
after callingnotify()
.
Step 4: Main Thread Receives Notification and Prints Result:
After receiving the notification, the main thread is woken up and re-enters the synchronized block.
The main thread now has access to the shared
total
variable and prints the result.
Final Output:
Main thread calling wait() method
Child thread started the calculation
Child thread giving notification call
Main thread got notification call
5050
Thread Scheduler:
The Thread Scheduler decides which thread gets CPU time. In this case, both threads (main and child) have the same priority, so the execution order may vary.
The main thread waits for the child thread to complete its calculation and send a notification.
The ThreadB object (
b
) is shared between the main and child threads. The main thread waits for the lock onb
to be released by the child thread.
Conclusion:
This example demonstrates how inter-thread communication works in Java. The main thread waits for the child thread to complete a task (calculating the sum of numbers) and notifies it when done. By using wait()
and notify()
, the threads can safely communicate and share data. This approach is essential when dealing with concurrency and ensuring thread synchronization in multithreaded environments.
Inter-Thread Communication Example Without Synchronization
In this section, we're exploring the behavior of threads in the absence of synchronization mechanisms like wait()
, notify()
, or synchronized blocks. We will observe how the program behaves when the main thread and the child thread operate without coordination.
Example 1: Main Thread Accessing Child's total
Before It Is Updated
In the following example, the main thread tries to access the total
variable of the child thread (ThreadB
) without synchronization. This causes the main thread to print 0
because the child thread has not yet updated the value of total
when the main thread accesses it.
class ThreadB extends Thread {
int total = 0;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
total += i;
}
}
}
public class Multithreading74 {
public static void main(String[] args) {
ThreadB b = new ThreadB();
b.start();
System.out.println(b.total); // Main thread prints '0'
}
}
Output:
0
Explanation:
The main thread accesses
b.total
immediately after starting the child thread.Since the child thread has not completed its calculation at this point, the value of
total
is still0
, which is printed by the main thread.
Example 2: Main Thread Sleeping for 2 Seconds Before Accessing total
In this example, the main thread explicitly sleeps for 2 seconds, allowing the child thread to complete its calculation. This ensures that the child thread finishes before the main thread prints the result. However, the main thread has no guarantee that it will always access the value after the child finishes, leading to unnecessary delays.
class ThreadB extends Thread {
int total = 0;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
total += i;
}
}
}
public class Multithreading74 {
public static void main(String[] args) throws InterruptedException {
ThreadB b = new ThreadB();
b.start();
// Main thread sleeps for 2 seconds
Thread.sleep(2000);
System.out.println(b.total); // Prints '5050'
}
}
Output:
5050
Explanation:
The main thread sleeps for 2 seconds using
Thread.sleep(2000)
, allowing enough time for the child thread to complete its task.After the sleep, the main thread prints the
total
value, which has been correctly updated by the child thread.Although this approach works, it is inefficient because the sleep time is arbitrary, and the main thread is effectively wasting CPU time while it sleeps.
Example 3: Using join()
Method to Wait for Child Thread
The join()
method can be used to ensure that the main thread waits for the child thread to finish before proceeding. However, the main thread is forced to wait for the child thread's entire execution, which may not always be ideal if the child thread is doing more work than necessary.
class ThreadB extends Thread {
int total = 0;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
total += i;
}
// Simulate some additional code that the child thread executes
// 1 lakh lines of code, for example
}
}
public class Multithreading74 {
public static void main(String[] args) throws InterruptedException {
ThreadB b = new ThreadB();
b.start();
// Main thread waits for the child thread to complete execution
b.join();
System.out.println(b.total); // Prints '5050'
}
}
Output:
5050
Explanation:
The main thread calls
b.join()
, which makes it wait until the child thread finishes executing itsrun()
method.After the child thread completes, the main thread prints the result.
While this works, it is not ideal because the main thread has to wait for the entire execution of the child thread, even if there were parts of the thread's code that were unnecessary to wait for.
Example 4: Using activeCount()
Method to Check Number of Active Threads
The Thread.activeCount()
method can be used to check how many threads are currently active. In the following code, we check the number of active threads while the child thread is executing.
class ThreadB extends Thread {
int total = 0;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
total += i;
}
}
}
public class Multithreading74 {
public static void main(String[] args) throws InterruptedException {
ThreadB b = new ThreadB();
b.start();
// Print the number of active threads
System.out.println("No of threads active: " + Thread.activeCount());
// Main thread waits for the child thread to finish
b.join();
System.out.println(b.total); // Prints '5050'
}
}
Output:
No of threads active: 2
5050
Explanation:
The
Thread.activeCount()
method returns the number of active threads in the JVM at the time it is called.In this case, the main thread and the child thread are both active, so the output shows
2
active threads.The main thread then waits for the child thread to finish using
b.join()
, and once the child thread finishes, the main thread prints the value oftotal
.
Conclusion:
Without synchronization or thread coordination, there can be unpredictable results when accessing shared data (
total
in this case), as the main thread may access it before the child thread has finished updating it.Using
Thread.sleep()
orjoin()
can work in some cases, but these approaches are inefficient and not ideal for ensuring correct and timely execution.Synchronization and proper thread communication (e.g., using
wait()
andnotify()
) are necessary to ensure proper coordination between threads and efficient CPU time usage.
Key Concepts:
Multithreading: In Java, multithreading allows the execution of multiple threads concurrently. Each thread represents a single path of execution, and Java allows you to run these threads independently, improving the performance of programs by utilizing multiple CPUs/cores.
wait()
andnotify()
Methods: These are used for inter-thread communication and synchronization, allowing threads to communicate or synchronize their actions to avoid data inconsistency.
Example 1: Main Thread Sleeping and Deadlock (Infinite Wait)
Problem:
// ThreadB class:
class ThreadB extends Thread {
int total = 0;
@Override
public void run() {
synchronized(this) {
System.out.println("Child thread started the calculation");
for (int i = 1; i <= 100; i++) {
total += i;
}
System.out.println("Child thread giving notification call");
this.notify(); // Notifying main thread
}
}
}
public class Multithreading75 {
public static void main(String[] args) throws InterruptedException {
ThreadB b = new ThreadB();
b.start(); // Start child thread
// Main thread is sleeping for 3 seconds
Thread.sleep(3000);
// Main thread tries to wait for the child thread to finish
synchronized(b) {
System.out.println("Main thread calling wait() method");
b.wait(); // Main thread will wait for the notification from the child thread
System.out.println("Main thread got notification call");
System.out.println(b.total);
}
}
}
Explanation:
Main Thread Sleeping (
Thread.sleep(3000)
): Here, the main thread is instructed to sleep for 3 seconds usingThread.sleep(3000)
. This introduces a delay in the execution of the main thread, giving the child thread a chance to start executing.Main Thread Calls
wait()
: The main thread then enters a synchronized block and calls thewait()
method on the child thread objectb
. This makes the main thread wait for the child thread to finish its task and notify the main thread.Deadlock (Infinite Wait): Since the main thread is sleeping for 3 seconds before calling
wait()
, it cannot receive the notification from the child thread while it’s asleep. As a result, the main thread remains in an infinite waiting state, creating a deadlock scenario.
Key Points:
Deadlock occurs when a thread is waiting indefinitely for a condition to be met, but the condition is never triggered.
In this case, the main thread cannot receive the notification because it is sleeping when the child thread finishes its calculation.
Example 2: Main Thread Waiting for 1 Second
Problem:
// ThreadB class:
class ThreadB extends Thread {
int total = 0;
@Override
public void run() {
synchronized(this) {
System.out.println("Child thread started the calculation");
for (int i = 1; i <= 100; i++) {
total += i;
}
System.out.println("Child thread giving notification call");
this.notify(); // Notifying main thread
}
}
}
public class Multithreading75 {
public static void main(String[] args) throws InterruptedException {
ThreadB b = new ThreadB();
b.start(); // Start child thread
// Main thread is sleeping for 1 second
Thread.sleep(3000);
synchronized(b) {
System.out.println("Main thread calling wait() method");
b.wait(1000); // Main thread waits for 1 second
System.out.println("Main thread got notification call");
System.out.println(b.total);
}
}
}
Explanation:
Main Thread Waiting for 1 Second (
wait(1000)
): In this version, the main thread callsb.wait(1000)
, meaning it will only wait for the child thread’s notification for 1 second. If the notification isn’t received within this time frame, the main thread resumes execution.Notification from Child Thread: The child thread calculates the sum of numbers from 1 to 100, then calls
this.notify()
to notify the main thread that it has completed its task.
Key Points:
wait(1000)
: This makes the main thread wait for a notification for a fixed amount of time (1 second). If it doesn’t get the notification within this time, it will resume execution, unlike the previous example where it waited indefinitely.Synchronization ensures that only one thread can execute the synchronized block at a time. This prevents race conditions where multiple threads try to modify shared resources simultaneously.
Output:
Child thread started the calculation
Child thread giving notification call
Main thread calling wait() method
Main thread got notification call
5050
Example 3: Child Thread Sleeping for 3 Seconds, Main Thread Waiting for 1 Second
Problem:
// ThreadB class:
class ThreadB extends Thread {
int total = 0;
@Override
public void run() {
synchronized(this) {
System.out.println("Child thread started the calculation");
for (int i = 1; i <= 100; i++) {
total += i;
}
try {
Thread.sleep(3000); // Child sleeps for 3 seconds before notifying
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Child thread giving notification call");
this.notify(); // Notify main thread
}
}
}
public class Multithreading75 {
public static void main(String[] args) throws InterruptedException {
ThreadB b = new ThreadB();
b.start(); // Start child thread
synchronized(b) {
System.out.println("Main thread calling wait() method");
b.wait(1000); // Main thread waits for 1 second
System.out.println("Main thread got notification call");
System.out.println(b.total);
}
}
}
Explanation:
Child Thread Sleeping for 3 Seconds: Here, the child thread first performs the calculation of the sum, then it goes to sleep for 3 seconds before calling
this.notify()
. This delay in notifying the main thread could cause the main thread to time out, as it is only waiting for 1 second (b.wait(1000)
).Main Thread Waits for 1 Second: The main thread calls
b.wait(1000)
and waits for 1 second. After that, if it hasn’t received a notification, it will wake up and resume execution. However, in this case, the child thread is still asleep when the main thread times out and doesn’t get the notification.Notification After 3 Seconds: After 3 seconds, the child thread calls
notify()
to wake up the main thread, but by then, the main thread has already resumed and is executing.
Key Points:
Timing Issues: There’s a potential race condition here where the main thread might timeout before the child thread has a chance to notify it, and vice versa.
Synchronization and Timing: This example illustrates the importance of ensuring proper synchronization and coordination between threads. The main thread should wait for as long as the child thread needs, and the child thread should not take too long before notifying the main thread.
Output:
Main thread calling wait() method
Child thread started the calculation
Child thread giving notification call
Main thread got notification call
5050
Key Takeaways:
wait()
andnotify()
: These methods are fundamental in managing thread communication. Thewait()
method causes the calling thread to stop execution until another thread callsnotify()
ornotifyAll()
on the same object.Thread.sleep()
: This method pauses the current thread for a specified number of milliseconds, but it doesn’t release the lock it holds. In multithreading, it's important to carefully manage the use ofsleep()
as it can cause timing issues or delays.Synchronization: Using the
synchronized
block ensures that only one thread can access a particular section of code at a time, preventing race conditions and ensuring that shared data is correctly managed.Deadlock and Race Conditions: Both deadlock and race conditions are potential issues when working with multithreaded programs. Deadlock happens when two threads are stuck waiting for each other indefinitely. Race conditions occur when the behavior of the program depends on the timing of thread execution, leading to unpredictable results.
Proper Timing: When using
wait()
, it’s important to handle timing carefully to avoid situations where a thread waits indefinitely or prematurely resumes execution.
Producer-Consumer Problem with Synchronization
The Producer-Consumer problem is a classic synchronization problem where the Producer thread creates or produces data, and the Consumer thread consumes the data. Both threads access a shared resource (such as a queue). Synchronization ensures that the Producer doesn't produce data when the queue is full and that the Consumer doesn't consume data when the queue is empty.
Conceptual Diagram of Producer-Consumer
Producer Queue Consumer
| | |
(produce item) | (consume item)
| | |
notify() | wait()
| | |
(item added) <--- | (queue empty)
wait() notify()
Code Example for Producer-Consumer Problem
Here is the complete working code to demonstrate the Producer-Consumer problem with synchronization:
Producer Class
import java.util.LinkedList;
import java.util.Queue;
class Producer extends Thread {
private Queue<Integer> queue;
public Producer(Queue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
if (queue.size() == 10) {
try {
System.out.println("Queue is full, producer is waiting...");
queue.wait(); // Wait if queue is full
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// Produce an item and add it to the queue
int item = (int) (Math.random() * 100);
queue.add(item);
System.out.println("Produced: " + item);
queue.notify(); // Notify consumer that item is available
}
}
}
}
Consumer Class
class Consumer extends Thread {
private Queue<Integer> queue;
public Consumer(Queue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
if (queue.isEmpty()) {
try {
System.out.println("Queue is empty, consumer is waiting...");
queue.wait(); // Wait if queue is empty
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// Consume an item from the queue
int item = queue.remove();
System.out.println("Consumed: " + item);
queue.notify(); // Notify producer that space is available
}
}
}
}
Main Class to Run the Example
public class Main {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
Thread producer = new Producer(queue);
Thread consumer = new Consumer(queue);
producer.start();
consumer.start();
}
}
Explanation of the Code:
Producer Thread:
The Producer produces items and adds them to the queue.
If the queue size exceeds 10, it calls
wait()
to make the Producer wait.After producing an item, it calls
notify()
to wake up the Consumer.
Consumer Thread:
The Consumer consumes items from the queue.
If the queue is empty, it calls
wait()
to wait for the Producer to add items.After consuming an item, it calls
notify()
to wake up the Producer.
Differences Between notify()
and notifyAll()
notify()
vs notifyAll()
notify()
:Notifies only one thread that is waiting on the object.
The thread chosen to wake up is selected by the thread scheduler, and it cannot be controlled.
notifyAll()
:Notifies all threads that are waiting on the object.
All threads are given a chance to acquire the lock and execute, one by one.
Example of notify()
and notifyAll()
Scenario 1: Using notify()
Object obj = new Object();
synchronized(obj) {
obj.wait(); // Thread waits for condition
}
// Notify one waiting thread to wake up
synchronized(obj) {
obj.notify(); // Only one thread gets the chance to run
}
Scenario 2: Using notifyAll()
Object obj = new Object();
synchronized(obj) {
obj.wait(); // Thread waits for condition
}
// Notify all waiting threads
synchronized(obj) {
obj.notifyAll(); // All threads are notified and will execute
}
IllegalMonitorStateException
The IllegalMonitorStateException occurs if you call wait()
, notify()
, or notifyAll()
on an object without acquiring the lock on that object. To use these methods, the thread must be synchronized on the object.
Example of IllegalMonitorStateException:
Stack s1 = new Stack();
Stack s2 = new Stack();
synchronized(s1) {
// Trying to call wait() on s2 without holding its lock
s2.wait(); // Throws IllegalMonitorStateException
}
Correct Usage:
Stack s1 = new Stack();
Stack s2 = new Stack();
synchronized(s2) {
// Correct: Synchronizing on s2 before calling wait
s2.wait(); // Works correctly as it synchronizes on s2
}
The thread must hold the lock of the object (in this case, s2
) when calling wait()
, notify()
, or notifyAll()
to avoid an IllegalMonitorStateException
.
Summary
Producer-Consumer Problem: Involves two threads (Producer and Consumer) sharing a queue. The Producer produces data, and the Consumer consumes it, with proper synchronization to prevent conflicts.
notify()
andnotifyAll()
: Used to wake up threads waiting on an object, but they differ in how many threads are notified.IllegalMonitorStateException: Raised if
wait()
,notify()
, ornotifyAll()
is called without acquiring the necessary lock on the object.
By understanding and applying synchronization with these methods, we can effectively manage multithreading in Java.
Other Series:
Connect with Me
Stay updated with my latest posts and projects by following me on social media:
LinkedIn: Connect with me for professional updates and insights.
GitHub: Explore my repository and contributions to various projects.
LeetCode: Check out my coding practice and challenges.
Your feedback and engagement are invaluable. Feel free to reach out with questions, comments, or suggestions. Happy coding!
Rohit Gawande
Full Stack Java Developer | Blogger | Coding Enthusiast