Table of contents
- 1. Interrupting a Thread
- 1.1 Introduction
- 1.2 What Happens When a Thread is Interrupted?
- 1.3 How Does One Thread Interrupt Another?
- 1.4 Real-World Analogy:
- 1.5 Code Example
- 1.6 Case-2: Main Thread Interrupting Child Thread Without Sleeping or Waiting State
- 1.7 Case-3: Main Thread Interrupting Child Thread After Entering Sleeping State
- 1.7.1 Scenario Description
- 1.7.2 Key Points
- 1.7.3 Real-World Analogy
- 1.7.4 Visualization
- 1.7.5 Code Example
- 1.7.6 Output
- 1.7.7 Explanation of Code Behavior
- 1.7.8 Key Takeaways
- Notes on Thread Methods and Behavior
- Comparison of Thread Methods: yield(), join(), sleep()
- Additional Notes
- 3. Creating a Thread Using the Runnable Interface
- 4. Creating a Thread Using Lambda Expressions
- Comparison Between Runnable and Lambda Expressions:
- 5. Creating Threads Using Anonymous Inner Classes
- Example Code (Using Anonymous Inner Class with Runnable)
- Explanation of the Code:
- Output (Order may vary):
- Comparison of Approaches:
- Key Observations:
- Using Lambda Expressions for Multithreading
- Code Explanation
- Code Implementation
- Output (Order may vary due to thread scheduling)
- Advantages of Using Lambda Expressions
- Comparison: Anonymous Class vs Lambda Expression
- Use Cases
- Explanation of the Multithreading Example with Display and MyThread
- Code Structure
- Code Walkthrough
- Key Concepts Illustrated
- Code Output
- Potential Enhancements
- Multithreading Example Explanation
- Detailed Explanation
- Execution Flow
- Output
- Key Concepts
- Enhancements
- Code Explanation
- Output Analysis
- Code Walkthrough
- Need for Synchronization
- Synchronized Output
- Case 2: Thread Operations with Locks
- Explanation of Synchronization in Java
- Example Program: Demonstrating the Need for Synchronization
- Explanation of the Program
- Important Points to Note
- Summary
- Synchronization in Multi-Threaded Scenarios
- Key Takeaways
- Expanded Explanation on Locks and Synchronization in Java
- 1. Locks and Intrinsic Locks (Object Monitor)
- 2. Object-Level Lock vs Method-Level Lock
- 3. Synchronized vs Non-Synchronized Methods
- 4. Thread Behavior in Synchronization
- 5. Lock Concept Applied to Methods
- 6. Rules for Lock and Synchronization
- 7. Purpose of Synchronization
- 8. Lock Limitations
- 9. Summary
- Understanding the Behavior in Detail
- Concept Breakdown
- Code Analysis
- How the Threads Behave
- Output Example
- Why the Output is Irregular?
- How to Fix Irregular Output?
- Solution: Synchronize Methods
- Result with Synchronization
- Conclusion
- Understanding the Behavior with Synchronized Methods: Object Level Lock
- Concept Breakdown
- Code Analysis
- Execution Flow
- Output
- Key Observations
- How Object-Level Lock Works Internally
- Conclusion
- Static Synchronized Methods: Class Level Lock
- Key Concepts
- Code Analysis
- Execution Flow
- Output
- Key Observations
- Difference Between Object-Level and Class-Level Locks
- Conclusion
- Static Synchronized vs Synchronized Methods
- Key Points
- Code Walkthrough
- Execution Flow
- Output Analysis
- Key Takeaways
- Difference Between Object-Level and Class-Level Locks
- Conclusion
- Why Use Synchronized Blocks?
- Levels of Locks in Synchronized Block
- Code Example: Demonstrating Synchronized Block
- Summary
In this chapter, we dive deeper into the advanced concepts of multithreading in Java, focusing on critical synchronization techniques and thread management. As multithreading allows concurrent execution of multiple tasks, ensuring proper coordination and consistency between threads becomes essential to avoid issues like race conditions or data inconsistencies.
We will explore the following key topics in detail:
Interrupting Threads
Understand how threads can be interrupted, how theinterrupt()
method works, and how to handleInterruptedException
.Synchronization in Java
Discover the importance of synchronization to ensure thread safety when multiple threads access shared resources. We'll cover:Synchronized Methods: Locking entire methods to prevent thread interference.
Synchronized Blocks: Enhancing performance by locking only critical sections of code instead of entire methods.
Levels of Synchronization Locks
Learn about different lock mechanisms and their use cases, including:Object-Level Locks (
this
lock)Class-Level Locks (
ClassName.class
lock)Specific Object Locks (custom objects as locks)
Performance Optimization with Synchronized Blocks
Explore how synchronized blocks improve application performance by locking only problematic code regions rather than entire methods.Common Pitfalls with Synchronization
We'll discuss common errors like trying to use primitive types for synchronization and provide correct approaches using wrapper classes.
By the end of this chapter, you will have a strong understanding of how to effectively use synchronization and thread management to build efficient, thread-safe Java applications. You’ll also gain clarity on where to use synchronized methods vs. synchronized blocks and how to handle multi-threaded scenarios efficiently.
Let’s get started! 🚀
1. Interrupting a Thread
1.1 Introduction
In multithreading, the interrupt()
method plays a crucial role in managing the lifecycle of threads, especially when one thread needs to signal another to stop its current activity or wake up from a waiting or sleeping state.
Key Points:
Method Definition: The
public void interrupt()
method is used to interrupt a thread.When to Use:
To interrupt a thread in a sleeping or waiting state.
To notify a thread to stop its execution or respond to an external event.
1.2 What Happens When a Thread is Interrupted?
When a thread is interrupted, the InterruptedException
is thrown if the thread is in a sleeping or waiting state.
Characteristics of InterruptedException
:
Definition: It is a checked exception that must be explicitly handled using a
try-catch
block.Scenario:
If a thread is in the waiting state, and another thread interrupts it using theinterrupt()
method, theInterruptedException
is thrown.
1.3 How Does One Thread Interrupt Another?
One thread can interrupt another by invoking the interrupt()
method. Let's explore this using an example where the main thread (parent) interrupts the child thread.
1.3.1 Case: Parent Thread Interrupting Child Thread
Scenario Description:
The main thread creates and starts a child thread.
The child thread performs a task and then enters a sleeping state for 3 seconds after each task iteration.
The main thread interrupts the child thread using the
interrupt()
method.When the child thread detects the interrupt signal, it throws an
InterruptedException
.
1.3.2 Visualization:
Refer to the picture at timestamp 20:49 (Image-1 in GitHub). Below is the textual representation of the thread states:
Thread Scheduler
_______|________
| |
main thread thread-0 (child thread)
| | -> (Sleeping / Waiting state)
t.interrupt() | |
| | InterruptedException is thrown
1.3.3 Key Observations:
Both threads (main and child) have the same priority, so the thread execution order is unpredictable.
Main Thread:
- Executes and calls
t.interrupt()
to interrupt the child thread.
- Executes and calls
Child Thread:
Sleeps for 3 seconds after each task execution.
When interrupted, it catches the
InterruptedException
.
The interrupt call will only be executed when the interrupted thread is in the sleeping/waiting state.
1.4 Real-World Analogy:
Imagine a scene from a movie where a snake wants to take revenge on a person. If the person falls asleep, the snake bites immediately. Similarly, if a thread is in a sleeping state, the interrupt call wakes it up by throwing an exception.
1.5 Code Example
Here’s a practical example demonstrating how one thread can interrupt another:
// Child thread class
class MyThread extends Thread {
@Override
public void run() {
try {
for (int i = 1; i <= 5; i++) {
System.out.println("I am sleeping thread " + i);
Thread.sleep(3000); // Sleep for 3 seconds
}
} catch (InterruptedException e) {
System.out.println("I got interrupted");
}
}
}
// Main class
public class MultithreadingExample {
public static void main(String[] args) {
MyThread t = new MyThread(); // Create a child thread
t.start(); // Start the child thread
// Interrupt the child thread
t.interrupt();
System.out.println("End of main method");
}
}
1.5.1 Output:
End of main method
I am sleeping thread 1
I got interrupted
1.5.2 Explanation of Code Behavior:
Main Thread:
Calls
t.interrupt()
immediately after starting the child thread.Ends its execution after printing "End of main method".
Child Thread:
Executes the loop and enters a sleeping state using
Thread.sleep(3000)
.When interrupted by the main thread, it catches the
InterruptedException
and prints "I got interrupted".
1.5.3 Key Takeaways:
The interrupt mechanism allows threads to communicate with each other efficiently.
Proper handling of
InterruptedException
is crucial for maintaining thread stability.
1.6 Case-2: Main Thread Interrupting Child Thread Without Sleeping or Waiting State
1.6.1 Scenario Description
In this case, the main thread (parent) interrupts the child thread using the interrupt()
method. However, unlike Case-1, the child thread never enters a sleeping or waiting state.
Key Points:
The
interrupt()
method is invoked, but since the child thread is not in a state where it can respond (e.g., sleeping/waiting), the interrupt call has no effect.Once the child thread completes its execution, it transitions to the dead state.
The
interrupt()
call is effectively wasted, as it cannot influence the child thread's execution in this scenario.
1.6.2 Real-World Analogy
Imagine a movie where a snake is waiting to attack a person, but the person dies naturally before the snake can act. The snake leaves without doing anything. Similarly, if a thread finishes its execution naturally without sleeping or waiting, the interrupt call has no effect.
1.6.3 Visualization
Refer to the diagram below to understand the thread states in this scenario:
Thread Scheduler
_______|________
| |
main thread thread-0 (child thread)
| (Not in Sleeping/Waiting state)
t.interrupt() | |
| |
| | Running state
|
1.6.4 Code Example
Here’s a practical example demonstrating this scenario:
// Child thread class
class MyThread extends Thread {
@Override
public void run() {
try {
for (int i = 1; i <= 5; i++) {
System.out.println("I am running thread " + i);
}
} catch (Exception e) {
System.out.println("I got interrupted");
}
}
}
// Main class
public class MultithreadingExample2 {
public static void main(String[] args) {
MyThread t = new MyThread(); // Create a child thread
t.start(); // Start the child thread
// Interrupt the child thread
t.interrupt();
System.out.println("End of main method");
}
}
1.6.5 Output
End of main method
I am running thread 1
I am running thread 2
I am running thread 3
I am running thread 4
I am running thread 5
1.6.6 Explanation of Code Behavior
Main Thread:
Calls
t.interrupt()
immediately after starting the child thread.Prints "End of main method" and finishes execution.
Child Thread:
Executes the loop and completes its task without entering a sleeping or waiting state.
Ignores the
interrupt()
call since it is irrelevant to its current state.Prints messages for all iterations and transitions to the dead state after completing execution.
1.6.7 Key Takeaways
Interrupt Call Usage:
- The
interrupt()
method only has an effect if the thread is in a sleeping or waiting state.
- The
No Effect in Running State:
- If a thread is actively running and completes its execution without sleeping or waiting, the
interrupt()
call is wasted.
- If a thread is actively running and completes its execution without sleeping or waiting, the
Error Handling:
- Even though the
try-catch
block is present, theInterruptedException
is not thrown because the thread never enters a state where the interrupt call is meaningful.
- Even though the
1.7 Case-3: Main Thread Interrupting Child Thread After Entering Sleeping State
1.7.1 Scenario Description
In this case, the main thread (parent) interrupts the child thread using the interrupt()
method after the child thread has started executing and entered a sleeping state. This results in an InterruptedException
, as the interrupt()
method disrupts the sleeping state of the child thread.
1.7.2 Key Points
The
interrupt()
method effectively interrupts the child thread only when it enters a sleeping or waiting state.An
InterruptedException
is generated because the child thread’s sleep state is interrupted by the main thread.The child thread handles the exception within a
try-catch
block.
1.7.3 Real-World Analogy
Imagine a scenario where a worker is doing their job and then takes a nap. The manager interrupts the nap midway, forcing the worker to wake up. This is analogous to a thread in a sleeping state being interrupted by another thread.
1.7.4 Visualization
Refer to the diagram below to understand the thread states in this scenario:
Thread Scheduler
_______|________
| |
main thread thread-0 (child thread)
| (Enters Sleeping after Task)
t.interrupt() | |
| |
Sleeping State
1.7.5 Code Example
Here’s an example to demonstrate this scenario:
// Child thread class
class MyThread extends Thread {
@Override
public void run() {
// Perform some task
for (int i = 1; i <= 5; i++) {
System.out.println("I am a lazy thread " + i);
}
// Enter sleeping state
System.out.println("Entering into sleeping state");
try {
Thread.sleep(3000); // Child thread sleeps for 3 seconds
} catch (InterruptedException e) {
System.out.println("I got interrupted"); // Handle interruption
}
}
}
// Main class
public class MultithreadingCase3 {
public static void main(String[] args) {
MyThread t = new MyThread(); // Create child thread
t.start(); // Start child thread
// Interrupt the child thread
t.interrupt();
// Main thread finishes execution
System.out.println("End of main method");
}
}
1.7.6 Output
I am a lazy thread 1
I am a lazy thread 2
I am a lazy thread 3
I am a lazy thread 4
I am a lazy thread 5
Entering into sleeping state
I got interrupted
End of main method
1.7.7 Explanation of Code Behavior
Main Thread:
Calls
t.interrupt()
while the child thread is running.Continues to execute and prints "End of main method."
Child Thread:
Executes its loop and completes the task of printing five iterations.
After completing its task, the thread enters a sleeping state using
Thread.sleep(3000)
.When the main thread interrupts, the sleeping state is disrupted, triggering an
InterruptedException
.The child thread catches the exception and prints "I got interrupted."
1.7.8 Key Takeaways
Interrupt Call is Effective:
- The
interrupt()
method successfully interrupts a thread if it is in a sleeping or waiting state.
- The
Handling
InterruptedException
:- Always handle
InterruptedException
in atry-catch
block to ensure the program doesn’t crash.
- Always handle
Thread Lifecycle:
- After handling the exception, the child thread can either terminate or continue with its execution, depending on the implementation.
Thread Coordination:
- Thread interruption is a mechanism for coordinating thread behavior in multithreading scenarios.
Notes on Thread
Methods and Behavior
General Notes:
If a thread interrupts another thread, but the target thread is not in a waiting/sleeping state, no exception will be thrown.
The
interrupt()
call will remain valid until the target thread enters a waiting/sleeping state, ensuring that the call is not wasted.Once the target thread enters a waiting/sleeping state, the
interrupt()
method will interrupt the thread and cause an exception (InterruptedException
).The
interrupt()
call is wasted only if the target thread never enters a waiting/sleeping state.
Comparison of Thread Methods: yield()
, join()
, sleep()
1) Purpose
yield()
: Pauses the current executing thread to give a chance to other threads of the same priority.join()
: A thread waits for another thread to complete its execution and join.sleep()
: A thread pauses its execution for a specified amount of time.
2) Is it Static?
yield()
: Yesjoin()
: Nosleep()
: Yes
3) Is it Final?
yield()
: Nojoin()
: Yessleep()
: No
4) Is it Overloaded?
yield()
: Nojoin()
: Yessleep()
: Yes
5) Does it Throw InterruptedException
?
yield()
: Nojoin()
: Yessleep()
: Yes
6) Is it a Native Method?
yield()
: Yesjoin()
: Nosleep()
:sleep(long ms)
: Yes (Native)sleep(long ms, int ns)
: No (Non-Native)
Additional Notes
- More
.class
files = Longer loading time:
Having a higher number of.class
files in your project increases the time required for class loading, which might affect application startup time.
3. Creating a Thread Using the Runnable Interface
Key Points:
Runnable Interface:
The
Runnable
interface is a functional interface defined in Java with a single abstract method,run()
.Any class that implements
Runnable
must provide an implementation for therun()
method, which contains the task to be executed by the thread.
@FunctionalInterface
interface Runnable {
void run(); // Abstract method
}
Advantages of Using
Runnable
:Java supports single inheritance, meaning a class cannot extend more than one class. Using
Runnable
avoids this limitation since the implementing class does not need to extendThread
.Encourages better separation of task logic and thread management by decoupling the task (defined in
Runnable
) from the thread itself (handled by theThread
class).
Impact of More
.class
Files:The number of
.class
files affects the application startup time.More
.class
files result in increased class-loading time during application initialization.
Example Code:
// Step 1: Define a class that implements Runnable
class MyRunnable implements Runnable {
public void run() {
// Child thread task
for (int i = 0; i <= 5; i++) {
System.out.println("Child Thread");
}
}
}
public class Multithreading47 {
public static void main(String[] args) {
// Step 2: Create an instance of MyRunnable
MyRunnable r = new MyRunnable();
// Step 3: Create a Thread and pass the Runnable instance
Thread t = new Thread(r);
// Step 4: Start the thread
t.start();
// Main thread task
for (int i = 0; i <= 5; i++) {
System.out.println("Main Thread");
}
}
}
Explanation:
Child Thread Creation:
An instance of the
MyRunnable
class (which implementsRunnable
) is passed to theThread
constructor.The
start()
method is invoked on theThread
object to initiate a new thread of execution.
Execution Flow:
- Once the thread is started, the JVM scheduler determines the execution order between the main thread and the child thread.
Output (Order may vary):
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Key Observations:
Interleaved Output:
Threads run independently, so their execution alternates depending on the thread scheduler.
The sequence of "Main Thread" and "Child Thread" messages may vary across different executions.
Thread Decoupling:
- By using
Runnable
, the logic of the thread (inrun()
) is decoupled from thread creation (Thread
class).
- By using
4. Creating a Thread Using Lambda Expressions
Key Points:
Runnable Interface and Lambda Expressions:
The
Runnable
interface is a functional interface, meaning it has a single abstract method:@FunctionalInterface interface Runnable { void run(); }
Since Java 8, functional interfaces can be implemented using lambda expressions, providing a more concise and readable syntax.
Benefits of Using Lambda Expressions:
Reduces boilerplate code for creating
Runnable
objects.Makes the code more compact and easier to read.
Avoids the need for separate class definitions when implementing
Runnable
.
Execution Context:
The
run()
method defined using a lambda expression is executed in the child thread.The main thread executes its task concurrently with the child thread.
Example Code (with Lambda Expression):
public class Multithreading48 {
public static void main(String[] args) {
// Step 1: Define a Runnable using a Lambda Expression
Runnable r = () -> {
for (int i = 0; i <= 5; i++) {
System.out.println("Child Thread");
}
};
// Step 2: Create a Thread object with the Runnable instance
Thread t = new Thread(r);
// Step 3: Start the child thread
t.start();
// Step 4: Main thread task
for (int i = 0; i <= 5; i++) {
System.out.println("Main Thread");
}
}
}
Explanation:
Defining the Task:
- A
Runnable
object is created using a lambda expression. Therun()
method inside the lambda defines the task for the child thread.
- A
Creating and Starting the Thread:
The
Runnable
object is passed to theThread
constructor, creating a new thread (t
).The
start()
method is invoked to begin execution of the child thread.
Concurrent Execution:
The
run()
method executes in the child thread.The main thread continues executing its own task concurrently.
Output (Order may vary):
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Key Observations:
Interleaved Output:
The order of execution between the main thread and the child thread is determined by the JVM's thread scheduler.
Hence, the exact sequence of "Main Thread" and "Child Thread" messages may vary.
Lambda Simplification:
The lambda expression eliminates the need for creating a separate class to implement the
Runnable
interface.The task is directly defined inline, making the code more concise and focused.
Comparison Between Runnable and Lambda Expressions:
Aspect | Traditional Runnable | Lambda Expression |
Syntax Complexity | Requires a separate class or anonymous inner class | Concise and inline definition |
Code Length | Longer | Shorter |
Readability | Moderate | High |
Java Version Support | Available in all Java versions | Requires Java 8 or higher |
5. Creating Threads Using Anonymous Inner Classes
Key Points:
Anonymous Inner Classes:
Anonymous inner classes allow you to create a one-time implementation of an interface or class without explicitly declaring a separate class.
This is useful when you need a quick implementation of the
Runnable
interface.
Advantages:
Reduces code clutter by avoiding separate class definitions.
Allows compact inline implementation.
Execution Context:
The
run()
method defined inside the anonymous inner class executes in the child thread.The main thread runs its task concurrently.
Example Code (Using Anonymous Inner Class with Runnable)
public class Multithreading49 {
public static void main(String[] args) {
// Step 1: Create an anonymous inner class for Runnable
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Child Thread");
}
}
};
// Step 2: Pass the Runnable object to a Thread
Thread t = new Thread(r);
// Step 3: Start the thread
t.start();
// Step 4: Main thread task
for (int i = 0; i <= 5; i++) {
System.out.println("Main Thread");
}
}
}
Alternative Approaches:
1. Inline Anonymous Class Definition:
Instead of creating a separate Runnable
object, you can directly pass the anonymous inner class as an argument to the Thread
constructor.
public class Multithreading49 {
public static void main(String[] args) {
// Step 1: Directly pass the anonymous inner class
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Child Thread");
}
}
});
// Step 2: Start the thread
t.start();
// Step 3: Main thread task
for (int i = 0; i <= 5; i++) {
System.out.println("Main Thread");
}
}
}
2. Using Method Chaining:
You can eliminate variable declarations entirely by chaining the new
operator and the start()
method call.
public class Multithreading49 {
public static void main(String[] args) {
// Step 1: Combine Thread creation and start in one statement
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Child Thread");
}
}
}).start();
// Step 2: Main thread task
for (int i = 0; i <= 5; i++) {
System.out.println("Main Thread");
}
}
}
Explanation of the Code:
Creating an Anonymous Inner Class:
The
Runnable
interface is implemented in an anonymous inner class where therun()
method is overridden.The task of the child thread is defined inside this
run()
method.
Passing to the Thread Constructor:
The anonymous
Runnable
object is passed to theThread
constructor.This associates the task with the thread.
Starting the Thread:
- The
start()
method invokes therun()
method of theRunnable
object in a new thread.
- The
Concurrent Execution:
- The child thread and the main thread execute their tasks concurrently, and the output order may vary.
Output (Order may vary):
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Comparison of Approaches:
Approach | Advantages | Disadvantages |
Separate Runnable Object | Reusable code, clearer structure | Slightly longer code |
Inline Anonymous Class | Compact, reduces boilerplate | Slightly less readable if overused |
Method Chaining | Most concise, eliminates variable declaration | Harder to debug or modify in complex scenarios |
Key Observations:
Multiple
.class
Files:- When an anonymous inner class is created, an additional
.class
file is generated, which can increase loading time for many anonymous classes.
- When an anonymous inner class is created, an additional
Thread Scheduling:
- The interleaved output of "Main Thread" and "Child Thread" is controlled by the JVM's thread scheduler, and the order is not guaranteed.
Use Cases:
Use anonymous inner classes for quick and non-repetitive tasks.
For reusable tasks, consider creating separate classes or use lambda expressions if Java 8 or above is available.
Using Lambda Expressions for Multithreading
Lambda expressions in Java, introduced in Java 8, simplify the implementation of functional interfaces. Since Runnable
is a functional interface (it has only one abstract method, run()
), we can use a lambda expression to replace the anonymous inner class.
Code Explanation
Lambda Expression:
The
Runnable
interface has only one abstract method:void run()
.A lambda expression can directly implement this method, reducing boilerplate code.
Single
.class
File:Unlike anonymous inner classes, lambda expressions do not generate a separate
.class
file for the implementation.The compiled lambda expression is treated as a method reference in the enclosing class.
Code Structure:
A new
Thread
object is created, and the lambda expression implementingRunnable
is passed as an argument.The
start()
method of theThread
object initiates the child thread.
Code Implementation
public class Multithreading50 {
public static void main(String[] args) {
// Creating and starting the thread using a lambda expression
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Child Thread");
}
}).start();
// Main thread task
for (int i = 0; i <= 5; i++) {
System.out.println("Main Thread");
}
}
}
Output (Order may vary due to thread scheduling)
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Advantages of Using Lambda Expressions
Feature | Details |
Compact Code | Eliminates the need for an anonymous inner class, resulting in cleaner code. |
Improved Readability | Reduces boilerplate code while clearly conveying the task's intent. |
No Additional .class Files | Unlike anonymous inner classes, lambda expressions do not generate separate .class files. |
Efficiency | Directly translates to a method reference in the enclosing class. |
Comparison: Anonymous Class vs Lambda Expression
Aspect | Anonymous Inner Class | Lambda Expression |
Syntax | Verbose | Concise |
File Generation | Generates an extra .class file | No extra .class file |
Code Readability | Less readable for small tasks | More readable |
Java Version | Supported in all versions | Requires Java 8 or later |
Use Cases
Anonymous Inner Class: Use if the task involves more than one abstract method (non-functional interfaces).
Lambda Expression: Prefer for functional interfaces like
Runnable
for simplicity and clarity.
Explanation of the Multithreading Example with Display
and MyThread
This code demonstrates how threads can interact with a shared resource (Display
class) in a Has-A Relationship context, where one class (MyThread
) contains a reference to another (Display
).
Code Structure
Display
Class:Contains the
wish(String name)
method.The method prints a greeting ("Good Evening:") followed by the name of the user, five times.
A
Thread.sleep(2000)
call introduces a 2-second delay between each iteration.
MyThread
Class:Extends the
Thread
class to represent a thread of execution.Has-A Relationship with
Display
(contains a referenceDisplay d
).The constructor initializes the
Display
object and theString name
.Overrides the
run()
method to execute thewish()
method ofDisplay
on a specificname
.
Main Method (
Multithreading51
):Creates an object of
Display
(shared resource).Creates an object of
MyThread
, passing theDisplay
object and the user's name ("sachin").Starts the thread using
t1.start()
, which internally calls therun()
method.
Code Walkthrough
Initialization:
At Line-1, a
Display
objectd
is created. This object acts as the shared resource for the thread.At Line-2, a
MyThread
objectt1
is created, passing theDisplay
objectd
and the name"sachin"
.
Thread Execution:
When
t1.start()
is called, the thread'srun()
method executes.Inside
run()
, thewish(String name)
method of theDisplay
class is called usingd.wish(name)
.The
wish()
method:Iterates 5 times.
Prints "Good Evening:" followed by the provided name (
sachin
).Pauses for 2 seconds between iterations using
Thread.sleep(2000)
.
Key Concepts Illustrated
Concept | Details |
Has-A Relationship | MyThread contains a reference to Display , allowing reuse and modularity. |
Thread Synchronization | While no explicit synchronization is used here, threads can interact with shared objects. |
Thread.sleep() | Demonstrates pausing a thread for a fixed duration. |
Thread Execution Order | Threads run concurrently, and the exact order depends on the thread scheduler. |
Code Output
Good Evening:sachin
Good Evening:sachin
Good Evening:sachin
Good Evening:sachin
Good Evening:sachin
- The output consists of the greeting repeated five times, with a 2-second pause between each line.
Potential Enhancements
Synchronization: If multiple threads access the
Display
object, synchronization should be added to avoid race conditions. For example:public synchronized void wish(String name) { // Method implementation }
Thread Safety: The current implementation is safe since only one thread (
t1
) accesses theDisplay
object. However, in multi-threaded scenarios, thread safety mechanisms likesynchronized
orReentrantLock
may be needed.Multiple Threads: Adding another thread (e.g.,
t2
) to pass a different name (e.g., "rohit") would show the need for synchronization:MyThread t2 = new MyThread(d, "rohit"); t2.start();
Multithreading Example Explanation
Code
class Display {
public void wish(String name) {
for (int i = 1; i <= 5; i++) {
System.out.print("Good Evening:");
try {
Thread.sleep(2000); // Pause for 2 seconds
} catch (InterruptedException e) {
}
System.out.println(name);
}
}
}
class MyThread extends Thread {
Display d; // Has-A Relationship
String name;
// MyThread Constructor
MyThread(Display d, String name) {
this.d = d; // Assign shared Display object
this.name = name; // Assign user name
}
@Override
public void run() {
d.wish(name); // Call wish method of Display
}
}
public class Multithreading51 {
public static void main(String[] args) {
// 1. Create an object of Display (shared resource)
Display d = new Display();
// 2. Create a thread with the shared Display object and "sachin"
MyThread t1 = new MyThread(d, "sachin");
// 3. Start the thread
t1.start();
}
}
Detailed Explanation
1. Display Class
Purpose: Acts as a shared resource to perform a specific task (wishing the user in this case).
Method:
wish(String name)
prints a greeting message ("Good Evening:") followed by the name.Introduces a 2-second delay between iterations using
Thread.sleep(2000)
.The method executes its logic five times in a loop.
2. MyThread Class
Relationship:
Demonstrates a Has-A Relationship, where
MyThread
contains a reference to theDisplay
class.This allows
MyThread
to call methods ofDisplay
and reuse its logic.
Constructor:
Accepts a
Display
object (d
) and aString
(name
).Initializes these fields so the
run()
method can access them.
run()
Method:Overrides
Thread.run
()
to define the thread's task.Calls the
wish()
method of theDisplay
class, passing the user's name.
3. Main Method
Line-1: Create a
Display
object:- Acts as a shared resource for threads.
Line-2: Create a
MyThread
object:- Pass the
Display
object (d
) and a name ("sachin"
) to the constructor.
- Pass the
Start the Thread:
t1.start()
starts the thread, internally calling itsrun()
method.The
run()
method callsd.wish("sachin")
, executing the logic in theDisplay
class.
Execution Flow
The
main
method creates aDisplay
object (d
).A
MyThread
object (t1
) is created, passing theDisplay
object and"sachin"
.t1.start()
triggers the thread execution, callingt1.run
()
.Inside
run()
,d.wish("sachin")
is executed:Prints "Good Evening:" followed by "sachin".
Waits for 2 seconds before repeating the message.
The loop in
wish()
runs five times, producing the output.
Output
Good Evening:sachin
Good Evening:sachin
Good Evening:sachin
Good Evening:sachin
Good Evening:sachin
Key Concepts
Concept | Explanation |
Has-A Relationship | Demonstrates reuse of the Display class logic by having its reference in MyThread . |
Thread.sleep() | Pauses the thread for a fixed duration (2000 ms or 2 seconds in this case). |
Thread Execution | The run() method defines the thread's behavior when start() is called. |
Shared Resource | The Display object can be shared between multiple threads (not shown here, but possible). |
Enhancements
Using Multiple Threads:
Create another thread with a different name to demonstrate concurrent execution:
MyThread t2 = new MyThread(d, "rohit"); t2.start();
Synchronization:
Add synchronization to the
wish()
method to prevent race conditions if multiple threads access theDisplay
object:public synchronized void wish(String name) { // Method logic }
Case-1: Multiple Threads Operating on the Same Java Object
(11) Thread Scheduler:
The Thread Scheduler in Java determines the execution of threads.
All threads (main, t1, t2) are created with the same priority because child threads inherit the priority of the parent thread (
main
).The scheduler employs context switching to allocate CPU time among threads, ensuring that the allocated time to the Java program is fully utilized.
(12) Main Thread:
The
main
thread initializes theDisplay
object and createst1
andt2
threads using theMyThread
class.Once the
main
thread completes its tasks (like callingstart()
fort1
andt2
), it enters the dead state because no further instructions remain in themain
method.
(13) Threads Operating on the Same Object:
The
Display
object (d
) is shared by botht1
andt2
.Each thread invokes the
wish()
method ond
. Since the method is not synchronized, both threads may access the method simultaneously, leading to unpredictable behavior.
Code Explanation
(14) Thread Creation and Initialization:
Line-1: The
Display
objectd
is created.Line-2:
MyThread
objectst1
andt2
are initialized with theDisplay
object and their respective names (sachin
andDhoni
).Line-3 and Line-4: Both threads (
t1
andt2
) are started using thestart()
method. This creates two child threads in addition to the main thread.
(15) Thread States:
After
t1.start()
, two threads exist:Main Thread
Child Thread-1 (t1)
After
t2.start()
, three threads exist:Main Thread
Child Thread-1 (t1)
Child Thread-2 (t2)
(16) Execution Flow:
The
wish()
method in theDisplay
class contains afor
loop that iterates 10 times.Each iteration:
Prints
"Good Morning: "
Sleeps for 2 seconds using
Thread.sleep(2000)
.
(17) Context Switching:
While one thread (e.g.,
t1
) is in the sleep state, the thread scheduler switches context to another thread (e.g.,t2
).This leads to interleaved output where
t1
andt2
execute thewish()
method alternately.
Output Analysis
(18) Irregular Output:
Since both threads act simultaneously on the same object without synchronization, the output appears unpredictable.
For example:
Good Morning: sachin
Good Morning: Dhoni
Good Morning: Dhoni
Good Morning: sachin
Both threads print
"Good Morning: "
followed by their respective names (sachin
orDhoni
).The sequence depends on how the thread scheduler switches between
t1
andt2
.
(19) Data Inconsistency:
Multiple threads accessing the same resource (
wish()
method ofDisplay
) can lead to data inconsistency or conflicts.Synchronization is required to ensure only one thread executes the critical section at a time.
Code Walkthrough
class Display {
public void wish(String name) {
for (int i = 1; i <= 10; i++) {
System.out.print("Good Morning: ");
try {
Thread.sleep(2000); // Simulates some processing
} catch (InterruptedException e) {
// Handle exception
}
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 Multithreading52 {
public static void main(String[] args) {
Display d = new Display(); // Line-1: Create Display object
MyThread t1 = new MyThread(d, "sachin"); // Line-2: Create Thread t1
MyThread t2 = new MyThread(d, "Dhoni"); // Line-2: Create Thread t2
t1.start(); // Line-3: Start t1
t2.start(); // Line-4: Start t2
}
}
Need for Synchronization
(20) Problem Without Synchronization:
Threads
t1
andt2
can access thewish()
method simultaneously, resulting in interleaved output.Synchronization ensures that only one thread can access the critical section (i.e.,
wish()
method) at a time.
(21) Adding Synchronization:
Modify the wish()
method in the Display
class to include the synchronized
keyword:
class Display {
public synchronized void wish(String name) { // Add synchronized keyword
for (int i = 1; i <= 10; i++) {
System.out.print("Good Morning: ");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// Handle exception
}
System.out.println(name);
}
}
}
Synchronized Output
(22) Expected Behavior with Synchronization:
Thread
t1
will complete all 10 iterations of thewish()
method before threadt2
starts executing.Output:
Good Morning: sachin
Good Morning: sachin
Good Morning: sachin
...
Good Morning: Dhoni
Good Morning: Dhoni
Good Morning: Dhoni
...
This ensures consistent and predictable output.
Case 2: Thread Operations with Locks
This section elaborates on how locking mechanisms help manage thread operations on shared resources, ensuring consistency and correctness.
2.1 Printer Example Visualization
Imagine a Printer (
Display
) shared by two departments:Thread
t1
(Math Department): Responsible for printing math papers.Thread
t2
(Science Department): Responsible for printing science books.
Problem Scenario:
Both threads (
t1
andt2
) operate on the same printer simultaneously, causing output overlap and irregular results.For instance, if
t1
begins printing "Math Paper: Page 1" whilet2
starts printing "Science Book: Chapter 1," the output might look like:Math Paper: ScienChapter 1ce Book: Page 1
Such data inconsistency makes the final output unreliable.
2.2 Problem of Data Inconsistency
Definition: Data inconsistency occurs when multiple threads simultaneously access or modify a shared resource without proper synchronization, leading to conflicts.
Layman's Analogy:
Imagine a biryani plate placed on a road.
Two dogs approach from opposite directions and start pulling at the plate simultaneously.
The biryani gets messed up because both dogs are acting without coordination.
The biryani plate represents the shared resource (e.g.,
Display
), and the two dogs representt1
andt2
.
This issue is termed the Biryani Inconsistency Problem in multithreading, where uncoordinated access to shared resources leads to a mess.
2.3 Solution to the Problem
The solution is to ensure only one thread operates on the shared object at a time.
Analogy: Allow only one dog at a time to eat from the plate. Once it finishes, the next dog can proceed.
In programming, this is achieved using locks to coordinate thread access to shared resources.
2.4 Lock Mechanism in Multithreading
Locks ensure thread safety by allowing only one thread to execute a critical section of code at any given time.
2.4.1 How Locks Work
Thread Execution with Locks:
Thread
t1
Execution:When thread
t1
starts, it acquires a lock on the shared resource (e.g.,wish()
method).This prevents any other thread (
t2
) from accessing the same resource.While
t1
executes,t2
remains in a waiting state.
Thread
t1
Completes Execution:- Once
t1
finishes its task, it releases the lock, allowingt2
to proceed.
- Once
Thread
t2
Execution:After
t1
releases the lock,t2
acquires it and begins its task.This process ensures mutual exclusion, preventing both threads from executing simultaneously on the same resource.
2.4.2 Locking in Java
To implement locking in Java, use the synchronized
keyword.
Lock acquisition:
- A thread that enters a synchronized method/block automatically acquires the lock.
Lock release:
- The lock is released when the thread exits the synchronized method/block or its execution completes.
2.5 Implementation of Locks
Example: Synchronized Method
class Display {
public synchronized void wish(String name) { // Lock applied
for (int i = 1; i <= 5; i++) {
System.out.print("Good Evening: ");
try {
Thread.sleep(2000); // Simulate processing
} catch (InterruptedException e) {
System.out.println("Thread interrupted: " + e.getMessage());
}
System.out.println(name);
}
}
}
Explanation:
The
synchronized
keyword ensures only one thread at a time can access thewish()
method.Threads
t1
andt2
operate sequentially, avoiding data inconsistency.
2.6 Advantages of Locking
Prevents Data Inconsistency:
- By ensuring mutual exclusion, locks eliminate conflicts and irregularities in thread output.
Thread-Safe Operations:
- Locks guarantee that threads operate safely on shared resources.
2.7 Disadvantages of Locking
Increased Waiting Time:
- While one thread holds the lock, others must wait, increasing the overall execution time.
Reduced Performance:
- Excessive waiting time can lead to poor utilization of CPU resources, degrading performance.
2.8 StringBuffer (Synchronized) vs. StringBuilder (1.5v)
StringBuffer:
It is synchronized, meaning at most one thread can operate on it at a time.
Ensures thread safety but increases waiting time, leading to reduced performance.
StringBuilder:
Introduced in Java 1.5 as a non-synchronized alternative.
Suitable when only one thread operates on the resource.
Offers better performance due to reduced overhead from synchronization.
2.9 Summary
Locks are crucial for managing shared resources in multithreaded environments to avoid data inconsistency.
While locks ensure thread safety, they may also lead to performance trade-offs.
Use alternatives like StringBuilder when thread safety is not a concern to enhance efficiency.
Explanation of Synchronization in Java
1. What is Synchronization?
Synchronization in Java refers to the mechanism that allows control over thread access to shared resources.
It ensures that only one thread at a time can access critical sections of code (methods or blocks), which helps prevent issues such as data inconsistency and thread interference.
2. Key Characteristics of Synchronization
synchronized
keyword: Applicable to methods and blocks in Java.- It locks the shared resource (object) so that only one thread can execute the synchronized block or method at a time.
Prevents Data Inconsistency: Ensures thread-safe operations on shared resources.
Performance Impact:
Synchronization increases waiting time for threads, potentially reducing overall system performance.
Recommendation: Avoid synchronization unless necessary.
3. How Synchronization Works Internally
The synchronization mechanism uses the lock concept internally.
Every object in Java has a lock (monitor) associated with it.
When a thread enters a synchronized method/block, it acquires the lock on the object.
Other threads must wait until the lock is released.
Once the thread completes execution, it releases the lock, allowing the next thread to proceed.
4. Why Use Synchronization?
Synchronization is crucial in multithreading for the following reasons:
Prevent Thread Interference: Ensures threads do not interfere with each other's operations on shared resources.
Prevent Consistency Problems: Maintains consistency of data by ensuring controlled access.
Example Program: Demonstrating the Need for Synchronization
Code
// Shared resource class
class Display {
// Synchronized method to prevent thread interference
public synchronized void wish(String name) {
for (int i = 1; i <= 5; i++) {
System.out.print("Good Morning: ");
try {
Thread.sleep(2000); // Simulate a delay
} catch (InterruptedException e) {
System.out.println("Thread interrupted: " + e.getMessage());
}
System.out.println(name);
}
}
}
// Thread class
class MyThread extends Thread {
Display d; // Has-A Relationship
String name;
// Constructor for MyThread
MyThread(Display d, String name) {
this.d = d;
this.name = name;
}
@Override
public void run() {
d.wish(name); // Access the shared resource
}
}
// Main class to demonstrate synchronization
public class Multithreading54 {
public static void main(String[] args) {
// Create a single shared Display object
Display d = new Display();
// Create threads sharing the same Display object
MyThread t1 = new MyThread(d, "sachin");
MyThread t2 = new MyThread(d, "Dhoni");
// Start threads
t1.start();
t2.start();
}
}
Output
Good Morning: sachin
Good Morning: sachin
Good Morning: sachin
Good Morning: sachin
Good Morning: sachin
Good Morning: Dhoni
Good Morning: Dhoni
Good Morning: Dhoni
Good Morning: Dhoni
Good Morning: Dhoni
Explanation of the Program
Shared Resource: The
Display
class represents the shared resource.- The
wish(String name)
method is declared as synchronized, ensuring that only one thread can execute it at a time.
- The
Thread Creation:
t1
andt2
are two threads, both operating on the sameDisplay
object (d
), demonstrating a Has-A relationship.
Synchronization in Action:
When
t1
callsd.wish("sachin")
, it acquires the lock on theDisplay
object.t2
must wait untilt1
completes its execution and releases the lock.
Output Order:
Thread
t1
completes all iterations beforet2
begins execution, ensuring consistent output:Good Morning: sachin (x5) Good Morning: Dhoni (x5)
Important Points to Note
Without Synchronization:
If the
wish()
method is not synchronized, threadst1
andt2
may execute simultaneously, leading to mixed output:Good Morning: sachin Good Morning: Dhoni Good Morning: sachin Good Morning: Dhoni ...
Synchronization and Performance:
- Synchronization resolves data inconsistency but can increase waiting time, especially in systems with high thread concurrency.
Best Practices:
Use synchronization only when necessary to avoid performance bottlenecks.
For single-threaded operations, prefer non-synchronized alternatives like
StringBuilder
overStringBuffer
.
Summary
Synchronization ensures thread-safe operations on shared resources.
The
synchronized
keyword is crucial for preventing data inconsistency but comes with performance trade-offs.The example demonstrates how synchronization effectively manages concurrent threads to produce consistent results.
Synchronization in Multi-Threaded Scenarios
Case 1: Multiple Threads Operating on a Single Object
When multiple threads operate on the same object, synchronization is necessary to prevent:
Thread interference: Where threads overwrite each other’s work.
Data inconsistency: When shared data is modified by different threads simultaneously.
Code Example:
class Display {
public synchronized void wish(String name) {
for (int i = 1; i <= 5; i++) {
System.out.print("Good Morning: ");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
}
}
}
class MyThread extends Thread {
Display d;
String name;
MyThread(Display d, String name) {
this.d = d;
this.name = name;
}
@Override
public void run() {
d.wish(name);
}
}
public class MultithreadingExample {
public static void main(String[] args) {
Display d = new Display(); // Single Display object
MyThread t1 = new MyThread(d, "Sachin");
MyThread t2 = new MyThread(d, "Dhoni");
t1.start();
t2.start();
}
}
Output (Synchronized):
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Dhoni
Good Morning: Dhoni
Good Morning: Dhoni
Good Morning: Dhoni
Good Morning: Dhoni
Case 2: Multiple Threads Operating on Multiple Objects
- When multiple threads operate on multiple objects, synchronization has no impact because each thread works independently on its respective object.
Code Example:
class Display {
public synchronized void wish(String name) {
for (int i = 1; i <= 5; i++) {
System.out.print("Good Morning: ");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
}
}
}
class MyThread extends Thread {
Display d;
String name;
MyThread(Display d, String name) {
this.d = d;
this.name = name;
}
@Override
public void run() {
d.wish(name);
}
}
public class MultithreadingExample {
public static void main(String[] args) {
Display d1 = new Display(); // First Display object
Display d2 = new Display(); // Second Display object
MyThread t1 = new MyThread(d1, "Sachin");
MyThread t2 = new MyThread(d2, "Dhoni");
t1.start();
t2.start();
}
}
Output:
Good Morning: Sachin
Good Morning: Dhoni
Good Morning: Sachin
Good Morning: Dhoni
Good Morning: Sachin
Good Morning: Dhoni
Good Morning: Dhoni
Good Morning: Sachin
Good Morning: Sachin
Good Morning: Dhoni
Key Takeaways
Single Object:
- Synchronization is required to avoid thread interference and data inconsistency.
Multiple Objects:
- Synchronization isn't required as threads work on different instances, ensuring no shared resource conflict.
Synchronization Overhead:
- While it resolves data inconsistency, it may impact performance by increasing the waiting time for threads.
Best Practices:
- Use synchronization only when necessary to maintain a balance between thread safety and performance.
Expanded Explanation on Locks and Synchronization in Java
Synchronization and locks in Java are mechanisms to ensure thread safety when multiple threads access shared resources. This is critical in multithreaded applications to avoid inconsistent or unpredictable outcomes.
1. Locks and Intrinsic Locks (Object Monitor)
In Java, every object has a monitor lock, which is tied to the object's lifecycle. This lock is used to regulate thread access to synchronized methods or blocks:
A thread acquires the lock before entering a synchronized method or block.
While a thread holds the lock, no other thread can enter any synchronized method on the same object.
Once the thread exits the synchronized method, the lock is released, allowing other threads to acquire it.
2. Object-Level Lock vs Method-Level Lock
Locks are tied to objects and not methods. This means:
The lock is at the object level.
If multiple threads operate on multiple objects, they can access synchronized methods simultaneously since each object has its lock.
Example:
class SharedResource {
synchronized void methodA() {
// Critical Section
}
synchronized void methodB() {
// Critical Section
}
}
public class Main {
public static void main(String[] args) {
SharedResource obj1 = new SharedResource();
SharedResource obj2 = new SharedResource();
Thread t1 = new Thread(() -> obj1.methodA());
Thread t2 = new Thread(() -> obj2.methodB());
t1.start();
t2.start();
}
}
- Here,
t1
operates onobj1
andt2
operates onobj2
. Since these are two different objects, there is no impact of synchronization.
3. Synchronized vs Non-Synchronized Methods
Synchronized Methods:
These methods require the thread to acquire the object's lock before execution.
Only one thread at a time can execute any synchronized method on the same object.
Example:
synchronized void updateResource() { // Critical Section }
Non-Synchronized Methods:
These methods do not require the lock.
Multiple threads can execute them simultaneously without interference.
Example:
void readResource() { // Non-Critical Section }
Scenario with Mixed Synchronization: If a thread holds the lock for a synchronized method, other threads can still execute non-synchronized methods of the same object concurrently.
4. Thread Behavior in Synchronization
Case 1: Multiple Threads, Single Object
When multiple threads operate on the same object:
If a thread is executing a synchronized method, other threads must wait for the lock to be released.
Non-synchronized methods can be executed by any thread without waiting.
Example:
class SharedResource {
synchronized void synchronizedMethod() {
System.out.println(Thread.currentThread().getName() + " has acquired the lock.");
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + " has released the lock.");
}
void nonSynchronizedMethod() {
System.out.println(Thread.currentThread().getName() + " is executing non-synchronized method.");
}
}
public class Main {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread t1 = new Thread(() -> resource.synchronizedMethod());
Thread t2 = new Thread(() -> resource.synchronizedMethod());
Thread t3 = new Thread(() -> resource.nonSynchronizedMethod());
t1.start();
t2.start();
t3.start();
}
}
Case 2: Multiple Threads, Multiple Objects
When multiple threads operate on different objects:
- Synchronization has no effect since each object has its own lock.
Example:
SharedResource obj1 = new SharedResource();
SharedResource obj2 = new SharedResource();
Thread t1 = new Thread(() -> obj1.synchronizedMethod());
Thread t2 = new Thread(() -> obj2.synchronizedMethod());
t1.start();
t2.start();
In this case, t1
and t2
can execute simultaneously because they are working with different locks.
5. Lock Concept Applied to Methods
Synchronized Area:
Critical sections where threads must update shared resources (e.g., updating a database, modifying shared variables).
Threads must acquire the object's lock to execute these sections.
Non-Synchronized Area:
Non-critical sections where threads only read shared resources (e.g., retrieving data, checking availability).
Threads do not require the lock, allowing parallel execution.
Example of a Reservation System:
class ReservationApp {
void checkAvailability() {
// Non-Synchronized Area: Multiple threads can check availability simultaneously.
}
synchronized void bookTicket() {
// Synchronized Area: Only one thread can book a ticket at a time.
}
}
6. Rules for Lock and Synchronization
Object Lock:
A thread must acquire an object's lock to execute any synchronized method on that object.
Other threads must wait if they try to access any synchronized method on the same object.
Releasing the Lock:
- The lock is released when the synchronized method completes execution or if an exception occurs.
Non-Synchronized Methods:
- These methods can be accessed by multiple threads even if another thread holds the object's lock.
Lock Scope:
- The lock applies to the object, not individual methods. All synchronized methods of an object share the same lock.
7. Purpose of Synchronization
Synchronization ensures thread safety when threads share and modify common resources.
It prevents race conditions, where the outcome of the program depends on the order of thread execution.
8. Lock Limitations
Locks cannot be applied to primitive data types.
int x = 10; synchronized(x) { // Compile-Time Error: Unexpected type // Code }
Locks can only be applied to objects and class types.
9. Summary
Synchronization is a vital concept to ensure data consistency in multithreaded applications.
Use synchronized methods or blocks only for critical sections where shared resources are updated.
Non-synchronized methods allow for better performance by enabling concurrent thread execution.
Understanding the Behavior in Detail
In this example, two threads (t1
and t2
) are operating on the same object of the Display
class without any synchronization. This results in unpredictable and interleaved output.
Concept Breakdown
Threads and Concurrent Execution:
In multithreading, multiple threads can execute methods concurrently.
If no synchronization is applied, the CPU schedules thread execution independently, based on its internal time-slicing mechanism.
This allows different threads to interleave their outputs, leading to irregular behavior.
Methods in the
Display
Class:displayChar()
: Prints characters from 'A' to 'K' with a delay of 2 seconds for each character.displayNumber()
: Prints numbers from 1 to 5 with a delay of 2 seconds for each number.
Thread Setup:
Thread t1 calls the
displayNumber()
method.Thread t2 calls the
displayChar()
method.Both threads share the same
Display
object and run concurrently.
Lack of Synchronization:
Because the
synchronized
keyword is not used, the methods are not protected from simultaneous execution.This means both
t1
andt2
can access their respective methods at the same time.As a result, the output from
displayChar()
anddisplayNumber()
interleaves.
Code Analysis
Here’s the critical portion of the code:
Display
Class
class Display {
public void displayChar() {
for (int i = 65; i <= 75; i++) {
System.out.print((char) i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
public void displayNumber() {
for (int i = 1; i <= 5; i++) {
System.out.print(i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
Thread-1 (MyThread1
)
class MyThread1 extends Thread {
Display d;
MyThread1(Display d) {
this.d = d;
}
@Override
public void run() {
d.displayNumber(); // Thread t1 executes this method
}
}
Thread-2 (MyThread2
)
class MyThread2 extends Thread {
Display d;
MyThread2(Display d) {
this.d = d;
}
@Override
public void run() {
d.displayChar(); // Thread t2 executes this method
}
}
Main Class
public class Multithreading58 {
public static void main(String[] args) {
Display d1 = new Display(); // Shared display object
// Two threads operating on the same object
MyThread1 t1 = new MyThread1(d1);
MyThread2 t2 = new MyThread2(d1);
t1.start(); // Start Thread-1
t2.start(); // Start Thread-2
}
}
How the Threads Behave
Thread-1 (
t1
):- Calls
displayNumber()
and starts printing numbers1, 2, 3, 4, 5
with a 2-second delay between each.
- Calls
Thread-2 (
t2
):- Calls
displayChar()
and starts printing charactersA, B, C, ... K
with a 2-second delay between each.
- Calls
Concurrent Execution:
Since the methods are not synchronized, both threads are allowed to run concurrently.
For example:
t1
may print1
.Then
t2
printsA
.t1
continues with2
, andt2
may printB
.
This interleaved execution causes the output to appear irregular.
Output Example
The output is not deterministic because it depends on the CPU’s thread scheduling. However, a sample output could look like this:
A1B2C3D4E5FGHIJK
Explanation:
t1
(printing numbers) andt2
(printing characters) take turns running.There is no fixed order in which the threads execute, leading to the output being mixed up.
Why the Output is Irregular?
Thread Scheduling:
The JVM’s thread scheduler determines when each thread gets CPU time.
It uses a mechanism like time-slicing or preemptive scheduling to allocate execution time for threads.
No Synchronization:
- Since the methods are not synchronized, both threads can access their respective methods simultaneously.
Sleep Method:
The
Thread.sleep(2000)
call introduces a 2-second delay after printing each character or number.However, this delay does not block the other thread, which continues execution.
How to Fix Irregular Output?
To ensure that the output from displayNumber()
and displayChar()
does not interleave, we need to use the synchronized
keyword.
Solution: Synchronize Methods
By adding the synchronized
keyword to the methods, only one thread will be allowed to access the methods of the Display
class at a time.
Modified Display
Class:
class Display {
public synchronized void displayChar() {
for (int i = 65; i <= 75; i++) {
System.out.print((char) i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
public synchronized void displayNumber() {
for (int i = 1; i <= 5; i++) {
System.out.print(i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
Result with Synchronization
With synchronization:
t1
will complete printing all numbers1, 2, 3, 4, 5
first.Only after
t1
completes,t2
will print the charactersA, B, C, ... K
.
Output:
12345ABCDEFGHIJK
Conclusion
Without synchronization, methods can be accessed concurrently, leading to mixed and irregular output.
By adding
synchronized
, we ensure that one thread completes its task before another thread begins execution.
This guarantees a sequential and predictable output.
Understanding the Behavior with Synchronized Methods: Object Level Lock
In this example, the methods displayNum()
and displayChar()
in the Display
class are declared as synchronized, which ensures that only one thread can access these methods at a time. Here, an object-level lock is used, meaning that threads must acquire a lock on the shared object Display
to execute its synchronized methods.
Concept Breakdown
Synchronized Methods:
Declaring a method as
synchronized
ensures that only one thread can execute that method at a time.The thread that invokes the method acquires a lock on the object (
this
), and no other thread can access any other synchronized method of the same object until the lock is released.
Object-Level Lock:
The lock is associated with the object (
Display
instance here).When a thread acquires the lock:
It executes the entire synchronized method without interruption from other threads.
Other threads attempting to execute synchronized methods on the same object must wait until the lock is released.
Thread Execution:
Thread
t1
invokesdisplayNum()
and acquires the lock on the sharedDisplay
object (d1
).Thread
t2
invokesdisplayChar()
but cannot execute it immediately because the lock is already held byt1
.Once
t1
completes thedisplayNum()
method, it releases the lock, allowingt2
to acquire the lock and executedisplayChar()
.
Code Analysis
Display Class (with synchronized methods):
class Display {
public synchronized void displayChar() {
for (int i = 65; i <= 75; i++) {
System.out.print((char) i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
public synchronized void displayNum() {
for (int i = 1; i <= 5; i++) {
System.out.print(i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
- The methods
displayChar()
anddisplayNum()
are synchronized, ensuring that only one thread can execute either method at a time.
Thread Classes:
class MyThread1 extends Thread {
Display d;
MyThread1(Display d) {
this.d = d;
}
@Override
public void run() {
d.displayNum(); // Thread-1 calls displayNum()
}
}
class MyThread2 extends Thread {
Display d;
MyThread2(Display d) {
this.d = d;
}
@Override
public void run() {
d.displayChar(); // Thread-2 calls displayChar()
}
}
MyThread1
: Runs thedisplayNum()
method.MyThread2
: Runs thedisplayChar()
method.
Both threads operate on the same Display
object (d1
).
Main Method:
public class Multithreading59 {
public static void main(String[] args) {
Display d1 = new Display(); // Shared Display object
// Two threads operating on the same object
MyThread1 t1 = new MyThread1(d1);
MyThread2 t2 = new MyThread2(d1);
t1.start(); // Start Thread-1
t2.start(); // Start Thread-2
}
}
Both
t1
andt2
share the sameDisplay
objectd1
.Since the methods are synchronized, only one thread can execute a method at a time.
Execution Flow
Thread-1 (t1) starts and acquires the lock on
d1
to executedisplayNum()
:- Prints numbers
1, 2, 3, 4, 5
with a 2-second delay between each.
- Prints numbers
Thread-2 (t2) tries to acquire the lock to execute
displayChar()
:t2
is blocked becauset1
holds the lock.
Once
t1
completes thedisplayNum()
method:t1
releases the lock.
Thread-2 (t2) now acquires the lock and executes
displayChar()
:- Prints characters
A, B, C, ... K
with a 2-second delay between each.
- Prints characters
Output
The output will be sequential because of the synchronization:
12345ABCDEFGHIJK
Explanation:
12345
is printed first becauset1
acquires the lock and executesdisplayNum()
completely.After
t1
completes,ABCDEFGHIJK
is printed becauset2
acquires the lock and executesdisplayChar()
completely.
Key Observations
Synchronized Methods:
- Ensure that only one thread can execute synchronized methods of a shared object at a time.
Object-Level Lock:
- Synchronization works at the object level (
d1
), meaning all synchronized methods of that object share the same lock.
- Synchronization works at the object level (
Thread Blocking:
- If one thread holds the lock, other threads trying to execute synchronized methods on the same object are blocked until the lock is released.
How Object-Level Lock Works Internally
When a thread invokes a
synchronized
method:- It acquires the object lock on the instance of the class.
While the lock is held:
- Other threads trying to call any synchronized method on the same object must wait.
After completing the method:
- The thread releases the lock, allowing other threads to acquire it.
Conclusion
In this example:
Synchronization ensures that
displayNum()
anddisplayChar()
are executed sequentially, one after the other.This eliminates the irregular, interleaved output seen in Example-1 where no synchronization was used.
By using synchronized
methods, the output is predictable and orderly:
12345ABCDEFGHIJK
Static Synchronized Methods: Class Level Lock
In this example, the methods displayNum()
and displayChar()
are declared as static synchronized. Unlike instance-level locks (object-level locks), static synchronized methods apply class-level locks. This ensures that only one thread can execute any static synchronized method across all instances of the class.
Key Concepts
Static Synchronized Methods:
When a method is declared as
static synchronized
, the lock is not acquired on an instance (object) but on the class object (Class
level).This means the lock is shared across all instances of the class.
Class-Level Lock:
The lock is acquired on the Class object of the class (
Display.class
in this case).If one thread acquires the class-level lock, all other threads trying to access any static synchronized method of that class are blocked until the lock is released.
Thread Behavior:
Since the
displayNum()
anddisplayChar()
methods are static synchronized:Only one thread can execute either method at any time.
If one thread executes
displayNum()
, other threads trying to executedisplayChar()
ordisplayNum()
must wait.
Code Analysis
Display Class (with static synchronized methods):
class Display {
public static synchronized void displayChar() {
for (int i = 65; i <= 75; i++) {
System.out.print((char) i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
public static synchronized void displayNum() {
for (int i = 1; i <= 5; i++) {
System.out.print(i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
Both
displayNum()
anddisplayChar()
arestatic synchronized
.They acquire a class-level lock (
Display.class
).
Thread Classes:
class MyThread1 extends Thread {
Display d;
MyThread1(Display d) {
this.d = d;
}
@Override
public void run() {
d.displayNum(); // Thread-1 calls static synchronized displayNum()
}
}
class MyThread2 extends Thread {
Display d;
MyThread2(Display d) {
this.d = d;
}
@Override
public void run() {
d.displayChar(); // Thread-2 calls static synchronized displayChar()
}
}
Thread-1 (
MyThread1
): CallsdisplayNum()
.Thread-2 (
MyThread2
): CallsdisplayChar()
.
Main Method:
public class Multithreading60 {
public static void main(String[] args) {
// Create a single Display object
Display d1 = new Display();
// Create two threads using the same Display object
MyThread1 t1 = new MyThread1(d1);
MyThread2 t2 = new MyThread2(d1);
// Start threads
t1.start();
t2.start();
}
}
Shared Object (
d1
): Botht1
andt2
operate on the sameDisplay
object.However, since the methods are static synchronized, the lock is acquired on the class (
Display.class
), not on the object (d1
).
Execution Flow
Thread-1 (
t1
) starts and calls thedisplayNum()
method:Acquires the class-level lock (
Display.class
).Prints numbers
1, 2, 3, 4, 5
with a 2-second delay between each.
Thread-2 (
t2
) calls thedisplayChar()
method:- It cannot execute immediately because the class-level lock is held by
t1
.
- It cannot execute immediately because the class-level lock is held by
Once Thread-1 (
t1
) completes thedisplayNum()
method:- Releases the class-level lock.
Thread-2 (
t2
) acquires the class-level lock and executesdisplayChar()
:- Prints characters
A, B, C, ... K
with a 2-second delay between each.
- Prints characters
Output
The output will be sequential due to the class-level lock:
12345ABCDEFGHIJK
Explanation:
Thread-1 acquires the class-level lock first and completes
displayNum()
→ Prints12345
.Thread-2 acquires the lock next and completes
displayChar()
→ PrintsABCDEFGHIJK
.
Key Observations
Static Synchronized Methods:
Use a class-level lock (
Class
object) instead of an instance-level lock.The lock is shared across all instances of the class.
Class-Level Lock:
- Ensures that only one thread can execute any
static synchronized
method of the class at a time, regardless of the instance.
- Ensures that only one thread can execute any
Thread Blocking:
- While one thread holds the class-level lock, other threads trying to access any static synchronized method must wait.
Difference Between Object-Level and Class-Level Locks
Aspect | Object-Level Lock | Class-Level Lock |
Lock Type | Lock on an instance (object). | Lock on the Class object. |
Scope | One object at a time. | Shared across all objects of a class. |
Synchronized Method | Non-static synchronized methods. | Static synchronized methods. |
Concurrency | Allows multiple threads to execute synchronized methods on different objects. | Ensures only one thread executes static synchronized methods across all objects. |
Conclusion
In this example:
Static synchronized methods use a class-level lock, ensuring sequential execution of the
displayNum()
anddisplayChar()
methods.The output is predictable and orderly:
12345ABCDEFGHIJK
Static Synchronized vs Synchronized Methods
In this example, one method requires an object-level lock (synchronized
) and the other method requires a class-level lock (static synchronized
). Since these two locks are independent of each other, threads operating on these methods do not block each other.
Key Points
Synchronized Method:
Acquires an object-level lock.
Only one thread can execute this method on a given object at a time.
Other threads can execute the same method on different objects simultaneously.
Static Synchronized Method:
Acquires a class-level lock.
This lock is shared across all instances of the class.
If one thread holds this lock, no other thread can execute any static synchronized method of the same class.
Independence of Locks:
The object-level lock and class-level lock are completely independent.
A thread executing a static synchronized method does not block another thread executing a non-static synchronized method.
Irregular Output:
Since the two locks are independent, both threads can run their methods simultaneously.
This leads to irregular interleaving of output, depending on the thread execution order.
Code Walkthrough
Display Class:
class Display {
public static synchronized void displayChar() { // Class-level lock
for (int i = 65; i <= 75; i++) { // ASCII 'A' to 'K'
System.out.print((char) i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
public synchronized void displayNum() { // Object-level lock
for (int i = 1; i <= 5; i++) {
System.out.print(i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
displayChar()
:Declared as
static synchronized
→ Class-level lock.Acquires lock on
Display.class
.
displayNum()
:Declared as
synchronized
→ Object-level lock.Acquires lock on the instance (
d1
).
Thread Classes:
class MyThread1 extends Thread {
Display d;
MyThread1(Display d) {
this.d = d;
}
@Override
public void run() {
d.displayNum(); // Calls object-level synchronized method
}
}
class MyThread2 extends Thread {
Display d;
MyThread2(Display d) {
this.d = d;
}
@Override
public void run() {
d.displayChar(); // Calls static synchronized method
}
}
Thread-1 (
MyThread1
) callsdisplayNum()
→ acquires object-level lock.Thread-2 (
MyThread2
) callsdisplayChar()
→ acquires class-level lock.
Since the locks are independent:
- Both threads can execute their respective methods simultaneously.
Main Method:
public class Multithreading61 {
public static void main(String[] args) {
Display d1 = new Display(); // Shared object
// Two threads
MyThread1 t1 = new MyThread1(d1); // Calls displayNum()
MyThread2 t2 = new MyThread2(d1); // Calls displayChar()
t1.start();
t2.start();
}
}
Execution Flow
Thread-1 (
t1
) starts and acquires the object-level lock ond1
.- Executes
displayNum()
→ prints numbers (1
,2
,3
, ...).
- Executes
Thread-2 (
t2
) starts and acquires the class-level lock.- Executes
displayChar()
→ prints characters (A
,B
,C
, ...).
- Executes
Since the two locks are independent:
Both methods can execute simultaneously, leading to an irregular output.
Example:
A1B23C4DE5FGHIJK
Output Analysis
Irregular Output:
A1B23C4DE5FGHIJK
Explanation:
A
is printed by Thread-2 (displayChar()
).1
is printed by Thread-1 (displayNum()
).B
and2
are printed in quick succession as both threads continue execution independently.The interleaving happens unpredictably because both threads run concurrently without blocking each other.
Key Takeaways
Static Synchronized vs Synchronized:
static synchronized
→ class-level lock.synchronized
→ object-level lock.
Independent Locks:
- A thread holding a class-level lock does not block threads trying to acquire an object-level lock and vice versa.
Concurrency:
- Since the locks are independent, both methods can execute simultaneously, causing irregular outputs.
Output Behavior:
- The order of output depends on the CPU scheduler and thread execution timing.
Difference Between Object-Level and Class-Level Locks
Aspect | Object-Level Lock | Class-Level Lock |
Scope | Lock on an instance (object). | Lock on the Class object. |
Method Type | Non-static synchronized methods. | Static synchronized methods. |
Concurrency | Allows multiple threads to execute methods on different objects. | Only one thread can execute static synchronized methods at a time. |
Conclusion
This example demonstrates:
Independent nature of object-level and class-level locks.
How two threads can execute different methods simultaneously.
The resulting irregular interleaving of output.
This explanation dives deep into synchronized blocks in Java. Here's a concise and clean version of the points with examples and explanations:
Why Use Synchronized Blocks?
Problem with Entire Method Synchronization:
If a method is declaredsynchronized
, the entire method becomes a critical section. This causes a performance bottleneck, especially if only a few lines of code (a specific region) are problematic.synchronized void m1() { // entire method locked region1(); region2(); // only this needs to be synchronized region3(); }
Solution with Synchronized Block:
A synchronized block allows us to lock only a specific section of the code. This improves performance because:Only the critical section (problematic region) is locked.
Other parts of the code can be executed by other threads simultaneously.
Levels of Locks in Synchronized Block
Current Object Lock (
this
)
Locks the current object.synchronized (this) { // Critical section }
Class-Level Lock (
ClassName.class
)
Locks the entire class. This is useful for static members or methods.synchronized (Display.class) { // Critical section for class-level lock }
Specific Object Lock
Locks a particular object passed as a reference.synchronized (objectReference) { // Critical section }
Code Example: Demonstrating Synchronized Block
Example-1: Locking Specific Object
class Display {
void displayNumbers() {
for (int i = 1; i <= 5; i++) {
synchronized (this) { // Locking only this region
System.out.print(i + " ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class MyThread extends Thread {
Display d;
MyThread(Display d) {
this.d = d;
}
public void run() {
d.displayNumbers();
}
}
public class MultithreadingExample {
public static void main(String[] args) {
Display d1 = new Display();
Display d2 = new Display();
MyThread t1 = new MyThread(d1);
MyThread t2 = new MyThread(d2);
t1.start();
t2.start();
}
}
Output:
Threads will execute displayNumbers()
independently on their objects because the lock is on this
(current object).
Example-2: Class-Level Lock
class Display {
static void displayCharacters() {
synchronized (Display.class) { // Class-level lock
for (char c = 'A'; c <= 'E'; c++) {
System.out.print(c + " ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class MyThread1 extends Thread {
public void run() {
Display.displayCharacters();
}
}
class MyThread2 extends Thread {
public void run() {
Display.displayCharacters();
}
}
public class ClassLevelLockExample {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.start();
t2.start();
}
}
Output:
Characters will print one at a time because both threads compete for the class-level lock.
Example-3: Error with Primitive Types
Synchronized blocks require an object reference, not a primitive type.
public class MultithreadingExample {
public static void main(String[] args) {
int x = 48;
synchronized (x) { // Error: x is a primitive
System.out.println(x);
}
}
}
Output:
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
int is not a valid type's argument for the synchronized statement
Corrected Code:
public class MultithreadingExample {
public static void main(String[] args) {
Integer x = 48; // Wrapper class
synchronized (x) {
System.out.println(x);
}
}
}
Output:
48
Summary
Use synchronized blocks for performance optimization when only specific regions of code need synchronization.
Choose the correct lock:
Current object (
this
) for instance-level locking.Class-level lock (
ClassName.class
) for static regions.Specific object (
objectReference
) for fine-grained locking.
Primitive types cannot be used as locks. Use wrapper classes instead.
Conclusion:
In this chapter, we explored some of the most essential aspects of multithreading synchronization in Java. Understanding how to manage thread execution and shared resources is vital for building robust, scalable applications. From interrupting threads to using synchronized methods and blocks, you now have a solid foundation in ensuring thread safety and data consistency in your programs.
Here’s a recap of the key points covered:
Interrupting Threads: We saw how the
interrupt()
method can be used to gracefully stop threads, allowing your program to respond to external events without causing resource leaks or undefined behavior.Static Synchronized Methods: By locking methods at the class level, we ensured that only one thread at a time can access a shared resource, which is crucial for preventing concurrency issues.
Combining Static and Instance Synchronized Methods: Understanding the interplay between class-level and object-level locks helped us see how they can work independently or create issues when not managed carefully.
Synchronized Blocks: We learned that locking just the critical sections of code using synchronized blocks instead of entire methods can significantly improve performance, allowing for more efficient thread execution.
Types of Synchronization Locks: Whether locking an object, a class, or a specific region of code, we explored various ways of controlling access to shared resources, ensuring that threads don’t collide and lead to data inconsistency.
Wrapper Classes in Synchronization: We also covered the need for using wrapper classes like
Integer
instead of primitive types for synchronization to avoid compilation errors, ensuring that our locks are applied correctly.
Through these topics, we’ve addressed the core challenges that arise in multithreading environments and provided solutions to manage them effectively. By applying these techniques, you can write code that not only performs well but also handles concurrency issues safely.
As you continue your journey with Java multithreading, keep these concepts in mind to enhance both the functionality and efficiency of your applications. In future chapters, we will explore more advanced topics and patterns in multithreading, but for now, you should have a strong understanding of how synchronization works and how to leverage it in your projects.
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