Multi Threading Java
Multi Threading Java
CPU : Often referred as brain of the computer, is responsible for executong the instructions
from the program. Ti perform the basic arithmetic logic , control and i/o operations specified
by the instruction.
CORE: A core is an individual processing unit within a CPU. Modern CPUs can have multiple
cores, allowing them to perform multiple tasks simultaneously.
Program: set of instructions written in a programming language that tells the computer how
to perform the specifiec task.
Process: is an instance of a program that is being executed. When a program runs, the OS
creates a process to manage its execution.
Thread: A thread is the smallest unit of execution within the process. A process can have
multiple threads, which share the same resources but can run independently.
ex: A web browser like google chrome might uses multiple threads for the different tabs,
with each tab running as a separate thread.
Multithreading: refers to the ability to execute multiple threads within a single process
concurrently. A web browser can use multitreading by having separate threads for rendering
the page, running JS, and managing user input. This makes the browser more responsive and
efficient.
Multithreading enhances the efficiency of multitasking by breaking down individual tasks in
to smaller sub-tasks or threads. These threads can be processed simultaneously, making
better use of CPUs capabilities.
In Single core system: Both threads and processes are managed by the OS scheduler
through time slicing and context switching to create the illusion of simulyaneous execution.
In Multi core system: Bothe threads and process can run in true parallel on a different cores,
with the OS scheduler distributing the tasks across the cores to optimize performance.
Time Slicing:
Def: Time slicing divides the CPU time into small intervals called time slices or quanta.
Function: The OS scheduler allocates these time slices to different processes and threads,
ensuring each gets a fair share of CPU time.
Purpose: This prevents any single process or thread from monopolizing the CPU, improving
responsiveness ans enabling concurrent execution.
Context Switching:
Def: It is the process of saving the state of a currently running process or thread and loading
the state of the next one to be executed.
Function: when the process or thread’s time slice expires, the OS scheduler performs the
context switch to move the CPUs focus to the another process or thread.
Purpose: This allows multiple processes and thread to share CPU, giving the appearance of
simultaneous execution on a single-core CPU or improving parrallelism on multi-core CPUs.
Diff:
Multithreading can be achieved through multithreading where each task is divided into
threads that are managed concurrently.
While
Multitasking typically refers to the running of multiple applications, multithreading is more
granular, dealing with multiple threads within the same application or process.
Multitasking operates at the level of processes , which are the operating system’s primary
units of execution.
Multithreading operates at the level of threads, which are smaller units within a process.
Multitasking involves manageing resources between completely separate programs, which
may have independently memory spaces and system resources.
Multithreading involes managing resources within a single program, where threads share
the same memory and resources.
MultiTasking allows us to run multiple applications simultaneously, improving the
productivity and system utilization.
MultiThreading allows a single application to perform multiple tasks at a same time,
improving application performance and responsivesness.
Ex: The office manager (Operating system) assigns different employees(process) to work on
diff projects(applications) simultaneously. Each employee works on a different project
independently. Within a single project (application), a team(processes) of
employees(threads) works on the different parts of the project at the same time,
collaborationg and sharing the resources.
Java provides the robust support for multithreading, allowing developers to create
applications that can perforrm multiple tasks simultaneously, improving performance and
responsiveness.
In Java, multithreading is the cincurrent execution of two or more threads to maximize the
utilization of the CPU.
Java’s multithreading capabilities are part of the java.lang package, making it easy to
implement concurrent execution.
In a single-core environment, Java’s multithreading is managed by the JVM and the OS,
which switch between threads to give the illusion of concurrency.
The threads share the single core, and time-slicing is used to manage thread execution.
In a multi-core environment, Java’s multithreading can take full advantage of the available
cores.
The JVM can distribute threads across multiple cores, allowing true parallel execution of
threads.
A thread is a lightweight process, the smallest unit of processing. Java supports
multithreading through its java.lang.Thread class and the java.lang.Runnable interface.
When a Java program starts, one thread begins running immediately, which is called the
main thread. This thread is responsible for executing the main method of a program.
Code:
public class Test {
public static void main(String[] args) {
System.out.println("Hello world !");
System.out.println(Thread.currentThread().getName());
}
}
Output: Hello world
Main
To create a new thread in Java, you can either extend the Thread class or implement the
Runnable interface.
2.Passing that thread to the main() method and an independent thread is created to
execute the world() class.
To create a new thread in java, you can either extend the “Thread” or implement the
“Runnable” interface.
In both cases, the run method contains the code that will be executed in the new thread.
Thread Lifecycle
The lifecycle of a thread in Java consists of several states, which a thread can move through
during its execution.
• New: A thread is in this state when it is created but not yet started.
• Runnable: After the start method is called, the thread becomes runnable. It’s ready
to run and is waiting for CPU time.
• Running: The thread is in this state when it is executing.
• Blocked/Waiting: A thread is in this state when it is waiting for a resource or for
another thread to perform an action.
• Terminated: A thread is in this state when it has finished executing.
Runnable vs Thread
In Java, both Runnable and Thread are used for creating and executing threads, but they
serve different purposes and offer distinct advantages.
Runnable: Using the Runnable interface allows you to separate the task (the code that
needs to be executed) from the thread itself. This approach is beneficial when you want your
class to extend another class since Java does not support multiple inheritance. Implementing
Runnable gives you flexibility, as you can pass a Runnable instance to a Thread object and
execute it. This is particularly useful for implementing a task that can be reused across
multiple threads.
Thread: Extending the Thread class directly is an option when you need to override its
methods, such as run() or start(). This method is straightforward but limits your class to
extend only Thread, as Java allows single inheritance. Use this approach when the task
inherently requires direct control over the thread, such as managing thread-specific
operations or overriding lifecycle methods.
In summary, use Runnable for better design flexibility and to separate task logic from thread
management, while Thread is suitable for direct control over thread behavior when
necessary.
Methods Of Thread
1. start()
Definition: Starts the execution of a thread. The Java Virtual Machine calls the run() method
of this thread.
Begins the execution of the thread. The Java Virtual Machine (JVM) calls the run() method of
the thread.
2. run()
Definition: The entry point for the thread's execution. Contains the code that will be
executed by the thread.
The entry point for the thread. When the thread is started, the run() method is invoked. If
the thread was created using a class that implements Runnable, the run() method will
execute the run() method of that Runnable object.
4. join()
Definition: Waits for this thread to die. The current thread will pause until the thread it joins
has completed its execution.
Waits for this thread to die. When one thread calls the join() method of another thread, it
pauses the execution of the current thread until the thread being joined has completed its
execution.
class JoinExample extends Thread {
public void run() {
System.out.println("Thread is running...");
}
}
5. setPriority(int newPriority)
Definition: Changes the priority of the thread. Priority is a value between
Thread.MIN_PRIORITY (1) and Thread.MAX_PRIORITY (10).
6. interrupt()
Definition: Interrupts a thread that is in a blocked state (like sleeping or waiting). It sets the
thread’s interrupt status.
8. yield()
Definition: Suggests that the currently executing thread should yield its execution time to
allow other threads of the same priority to execute.
Thread.yield() is a static method that suggests the current thread temporarily pause its
execution to allow other threads of the same or higher priority to execute. It’s important to
note that yield() is just a hint to the thread scheduler, and the actual behavior may vary
depending on the JVM and OS.
10. getPriority()
Definition: Retrieves the priority of the thread.
class WaitNotifyExample {
private final Object lock = new Object();
try {
Thread.sleep(1000); // Ensure the waiting thread is waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(example::notifyMethod).start(); // Notify the waiting thread
}
}
These examples illustrate the functionality of each method in the context of threading in
Java.
13. Thread.setDaemon(boolean)
Definition: This method marks the thread as either a daemon thread or a user thread.
Daemon threads are those that do not prevent the JVM from exiting when the program
finishes executing. If the only threads running are daemon threads, the JVM will exit.
Example
Here's a simple example demonstrating the use of setDaemon(boolean):
Importance of setDaemon(boolean)
1. Resource Management: Daemon threads are typically used for background tasks,
such as garbage collection, monitoring, or handling resources. They can help manage
resources without keeping the application alive unnecessarily.
2. Application Exit: Since daemon threads do not prevent the JVM from exiting, they
are useful for tasks that should run only as long as the application runs. This helps
avoid resource leaks or hanging processes.
3. Lifecycle Control: It allows developers to control the lifecycle of threads based on the
needs of the application. Daemon threads can be useful for long-running background
tasks that do not need to block the program from exiting.
Summary
Using Thread.setDaemon(boolean) provides a way to define thread behavior in relation to
the JVM's lifecycle. Understanding how to properly use daemon threads is important for
efficient application design and resource management.
Counter.java
package MultiThreading.synchronization;
@Override
public void run(){
for (int i=0; i<1000; i++){
counter.increament();
}
}
}
Main.java
package MultiThreading.synchronization;
try{
t1.join();
t2.join();
}catch (Exception e){
Here in this program, The Output could be varied which results in <=2000 (output<=2000).
Because the Main method created 2 threads and both of them executing the Counter class
concurrently.
Since the increment() method in the Counter class is not synchronized. This results in a race
condition when both threads try to increment the count variable concurrently.
Without synchronization, one thread might read the value of count before the other thread
has finished writing its incremented value. This can lead to both threads reading the same
value at the same time, incrementing it at the same time, and writing it back at the same
time, effectively losing one of the increments.
Therefore, the Synchronization comes into play.
Synchronization
Synchronization in programming, particularly in Java, is a mechanism that ensures that
multiple threads can safely access shared resources without causing data inconsistency or
corruption. It allows only one thread to execute a particular section of code at a time,
ensuring that shared data remains consistent and preventing issues like race conditions.
Key Points:
1. Mutual Exclusion: Synchronization provides mutual exclusion, meaning that only one
thread can access the synchronized block or method at any given time.
2. Critical Sections: Parts of code that modify shared resources should be synchronized
to protect them from concurrent access.
3. Types of Synchronization:
o Synchronized Methods: Whole methods are synchronized, allowing only one
thread to execute them on an object.
class Counter {
private int count = 0;
// Synchronized method
public synchronized void increment() {
count++;
}
}
class Counter {
private int count = 0;
public void increment() {
// Synchronized block
synchronized (this) {
count++;
}
}
o
4. Java Constructs: Java provides several constructs for synchronization, including the
synchronized keyword and more advanced tools in the java.util.concurrent package.
5. Visibility: Synchronization ensures that changes made by one thread are visible to
others, preventing stale data.
6. Deadlock: Improper use of synchronization can lead to deadlocks, where two or
more threads are waiting indefinitely for each other to release resources.
By using synchronization, developers can create safe and reliable multi-threaded
applications that avoid common pitfalls associated with concurrent programming.
By synchronizing the increment() method, you ensure that only one thread can execute this
method at a time, which prevents the race condition. With this change, the output will
consistently be 2000.
Lock in Java
In Java, a lock is a synchronization mechanism that allows threads to control access to
shared resources. Locks provide a way to enforce mutual exclusion, ensuring that only one
thread can access a particular resource or section of code at any given time. This helps
prevent data inconsistencies and race conditions in multi-threaded applications.
The synchronized keyword in Java offers a basic level of thread safety, but it comes with
several drawbacks:
First, it locks the entire method or block of code, which can lead to performance bottlenecks
when multiple threads compete for access. Additionally, it lacks a try-lock feature, meaning
that threads can end up blocking indefinitely if they can't acquire the lock, which increases
the likelihood of deadlocks. Furthermore, synchronized only allows for a single monitor per
object, limiting its support for multiple condition variables and providing only basic
wait/notify mechanisms.
On the other hand, explicit locks implemented through the Lock interface present in
java.util.concurrent.locks package provide more granular control over synchronization. They
offer a try-lock capability that enables threads to attempt to acquire a lock without getting
stuck, thus helping to avoid blocking. Moreover, explicit locks support multiple condition
variables, facilitating more sophisticated thread coordination. This flexibility makes them a
more powerful choice for managing concurrency in complex applications.
BankAccount class.
package MultiThreading.lock;
This class defined here is having a method withdraw() which is performing some actions and
since this method is defined synchronised as well so that only one thread can access this
method at a time.
Main class
package MultiThreading.lock;
Using this Main class there 2 threads are created and wants to access the withdraw()
method, but due to being synchronized, only one thread can access withdraw() method at a
time.
Problem arises when any of the thread starts executing the methods and took infinte time
to execute it.
Then the problem of deadlock and starvation will arise and second thread will not get the
chance for executing that method.
Extra to Understand:
Understanding this method:
This code snippet creates an instance of an anonymous inner class that implements the
Runnable interface in Java. Here's a breakdown of the code:
Breakdown of the Code
1. Runnable Interface:
o The Runnable interface is a functional interface in Java that contains a single
method, run(). It is designed to represent a task that can be executed by a
thread.
2. Anonymous Inner Class:
o The new Runnable() { ... } syntax defines an anonymous inner class that
implements the Runnable interface without explicitly naming the class.
o This is often used for quick, one-time tasks where creating a separate class is
unnecessary.
3. Override Method:
o Inside the curly braces { ... }, the run() method is overridden. This method
contains the code that will be executed when the thread runs.
o In this case, the run() method calls sbi.withdraw(50);, indicating that it
attempts to withdraw 50 units (e.g., dollars) from an account represented by
sbi.
4. sbi:
o The sbi object is likely an instance of a class that has a withdraw(int amount)
method, which presumably handles the logic for withdrawing money from a
bank account or similar structure.
Functional Interface:
A functional interface in Java is an interface that has exactly one abstract method. This
allows the interface to be implemented using a lambda expression or method reference,
which makes it easier to write concise and readable code, especially when dealing with
functional programming concepts.
Examples:
• Java Built-in Functional Interfaces: Java provides several built-in functional interfaces
in the java.util.function package, such as:
Example:
1. Implementing an Interface
2. Extending a Class:
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
Lock Interface
The Lock interface in Java is part of the java.util.concurrent.locks package and provides a
more sophisticated and flexible locking mechanism than the traditional synchronized
keyword. It is designed for managing concurrent access to shared resources in a multi-
threaded environment.
4. Lock Interruptibly:
o You can acquire a lock in a way that allows the thread to be interrupted while
waiting for the lock, using the lockInterruptibly() method.
5. Condition Variables:
o The Lock interface supports condition variables via the Condition interface,
allowing threads to wait for certain conditions to be met before proceeding.
This is more flexible than the single monitor available with synchronized.
Condition condition = lock.newCondition();
lock.lock();
try {
while (!conditionMet) {
condition.await(); // Wait for the condition
}
} finally {
lock.unlock();
}
o
Now writing the code of class BankAccount without using the synchronized keyword and
implementing the Lock interface:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
Explanation of Code
This code defines a BankAccount class that uses Java's ReentrantLock to manage concurrent
access to the account's balance, ensuring thread safety during withdrawals. Here’s a
breakdown of how it works:
Key Components:
1. Balance:
if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
o The tryLock method attempts to acquire the lock for up to 1000 milliseconds.
If another thread has the lock, this thread will wait for up to that time. If the
lock is not acquired within that time, it will proceed without blocking
indefinitely.
2. Balance Check:
Example:
The ReentrantLock() lock will keep on counting that how many times the lock is aquired and
how many times it released.
If the count is Zero then only the the second thread can enters otherwise it cannot enter.
Methods of ReentrantLock
lock()
• Acquires the lock, blocking the current thread until the lock is available. It would
block the thread until the lock becomes available, potentially leading to situations
where a thread waits indefinitely.
• If the lock is already held by another thread, the current thread will wait until it can
acquire the lock.
• public class BankAccount {
private int balance = 100;
private final Lock lock = new ReentrantLock();
System.out.println(Thread.currentThread().getName() + " is
withdrawing " + amount);
balance -= amount;
System.out.println(Thread.currentThread().getName() + " is
withdrawing " + amount);
balance -= amount;
unlock()
• Releases the lock held by the current thread.
• Must be called in a finally block to ensure that the lock is always released even if an
exception occurs.
lockInterruptibly()
• Acquires the lock unless the current thread is interrupted. This is useful when you
want to handle interruptions while acquiring a lock.
• The lockInterruptibly() method in Java's ReentrantLock class allows a thread to
acquire a lock, but it can be interrupted if the thread is waiting to acquire the lock.
This is particularly useful in scenarios where you want to allow a thread to be
interrupted (for example, when the thread is waiting on a lock for a long time).
• import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private int balance = 100;
private final Lock lock = new ReentrantLock();
thread1.start();
thread2.start();
What is Interruption?
Interruption in programming refers to the ability to stop a running thread from what it's
currently doing, allowing it to respond to a signal that indicates it should stop or change its
course of action.
Key Points:
1. Request to Stop: When you interrupt a thread, you're essentially sending it a request
to stop its current work. This doesn't forcibly terminate it but gives it a chance to
finish up or handle the interruption.
2. Use Case: For example, if a thread is waiting for a lock or sleeping, you can interrupt
it, and it will stop waiting or sleeping and handle the interruption, often by throwing
an InterruptedException.
3. Graceful Handling: A well-designed program will check for interruptions regularly
and can safely clean up resources or finish tasks before stopping.
Example in Real Life:
Think of a thread as a person working on a task. If you tap them on the shoulder (interrupt),
they might look up and decide to stop what they're doing, check in with you, or finish up
their task before moving on.
In programming, interruptions help manage how threads operate, especially in responsive
applications where you might need to cancel ongoing tasks or processes based on user
actions or other events.
Thread.currentThread().interrupt()
When an interruption occurs within the try block (for example, when a thread is waiting,
sleeping, or blocked), it typically throws an InterruptedException. When this exception is
caught, the thread's interrupted status is automatically cleared (set to false).
Purpose of Thread.currentThread().interrupt();
1. Restores Interrupted Status: By calling Thread.currentThread().interrupt(); in the
catch block, you are effectively restoring that interrupted status back to true. This is
important because:
o It allows any higher-level code or loops that check the interrupt status to
recognize that the thread was interrupted and handle it appropriately.
2. Graceful Handling: Keeping track of the interrupt status is essential in a multi-
threaded environment. If the thread was supposed to stop or take some action due
to an interruption, restoring the status enables that behavior.
Summary
So yes, you're right! The line serves to "keep a record" of the interruption by setting the
thread's status back to interrupted, allowing the thread or other parts of the program to
react properly to the interruption later on.
Fairness of Locking:
Fairness of Locking refers to a policy that determines how threads acquire locks when
competing for the same resource. In Java, the ReentrantLock class allows you to specify
whether the lock should be fair or unfair.
Fair vs. Unfair Locking
1. Fair Locking:
o When a ReentrantLock is set to fair, it grants access to the longest-waiting
thread when a lock becomes available.
o This helps prevent thread starvation, where some threads may never get a
chance to acquire the lock.
Example:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
t1.start();
t2.start();
t3.start();
}
}
2. Unfair Locking:
o When a ReentrantLock is created without fairness or with fairness set to false,
the lock does not guarantee any particular order of access. Threads can
acquire the lock in any order, potentially leading to higher throughput but
also the risk of thread starvation.
Example:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
t1.start();
t2.start();
t3.start();
}
}
Read-Write Locking
Read/Write Locking is a concurrency control mechanism that allows multiple threads to
read shared data simultaneously while restricting write access to ensure data integrity. It
differentiates between read and write operations, allowing for more efficient access patterns
in scenarios where reads are more frequent than writes.
Key Concepts
1. Read Lock:
o Allows multiple threads to read the shared resource concurrently.
o When a thread acquires a read lock, other threads can also acquire read locks
but cannot acquire a write lock until all read locks are released.
2. Write Lock:
o Allows only one thread to write to the shared resource at a time.
o When a thread acquires a write lock, no other thread can acquire either a
read or a write lock until the write lock is released.
Benefits
• Increased Concurrency: By allowing multiple readers, read/write locks can
significantly improve performance in read-heavy applications.
• Data Integrity: Write locks ensure that data is not modified while being read,
preventing inconsistencies.
Example in Java
Java provides the ReentrantReadWriteLock class in the java.util.concurrent.locks package,
which implements read/write locking. Here's a simple example:
1. import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
System.out.println(Thread.currentThread().getName() + "
incremented");
}
}
};
writerThread.start();
readerThread1.start();
readerThread2.start();
writerThread.join();
readerThread1.join();
readerThread2.join();
Deadlock Situation
A deadlock is a situation in multithreading where two or more threads are blocked forever,
each waiting for the other to release a resource. In other words, a deadlock occurs when
two or more threads cannot proceed because they are each waiting for the other to release
a resource that they need to continue executing.
A deadlock occurs in concurrent programming when two or more threads are blocked
forever, each waiting for the other to release a resource. This typically happens when
threads hold locks on resources and request additional locks held by other threads.
For example, Thread A holds Lock 1 and waits for Lock 2, while Thread B holds Lock 2 and
waits for Lock 1. Since neither thread can proceed, they remain stuck in a deadlock state.
Deadlocks can severely impact system performance and are challenging to debug and
resolve in multi-threaded applications.
Conditions for Deadlock
For a deadlock to occur, the following four conditions must be true simultaneously:
1. Mutual Exclusion: At least one resource must be held in a non-shareable mode. If
another thread requests that resource, it must be blocked until the resource is
released.
2. Hold and Wait: A thread holding at least one resource is waiting to acquire additional
resources that are currently being held by other threads.
3. No Preemption: Resources cannot be forcibly taken from a thread holding them;
they must be voluntarily released by the thread holding them.
4. Circular Wait: There exists a set of threads {T1, T2, ..., Tn} such that T1 is waiting for a
resource held by T2, T2 is waiting for a resource held by T3, and so on, with Tn
waiting for a resource held by T1.
Example of Deadlock in Java
Here’s a simple example demonstrating a deadlock situation:
1. class Pen {
public synchronized void writeWithPenAndPaper(Paper paper) {
System.out.println(Thread.currentThread().getName() + " is
using pen " + this + " and trying to use paper " + paper);
paper.finishWriting();
}
class Paper {
public synchronized void writeWithPaperAndPen(Pen pen) {
System.out.println(Thread.currentThread().getName() + " is
using paper " + this + " and trying to use pen " + pen);
pen.finishWriting();
}
@Override
public void run() {
pen.writeWithPenAndPaper(paper); // thread1 locks pen and
tries to lock paper
}
}
@Override
public void run() {
paper.writeWithPaperAndPen(pen); // thread2 locks paper
and tries to lock pen
}
}
public class DeadlockExample {
public static void main(String[] args) {
Pen pen = new Pen();
Paper paper = new Paper();
thread1.start();
thread2.start();
}
}
Class Definitions
1. Pen Class:
o Contains synchronized methods that represent actions involving a pen.
o writeWithPenAndPaper(Paper paper): This method attempts to use both the
pen and the paper. It locks the Pen object and tries to call
paper.finishWriting().
o finishWriting(): This method signifies that the writing action is complete.
2. Paper Class:
o Similar to the Pen class, it has synchronized methods for actions involving
paper.
o writeWithPaperAndPen(Pen pen): This method locks the Paper object and
tries to call pen.finishWriting().
o finishWriting(): Indicates that the writing action with paper is complete.
Runnable Tasks
3. Task1:
o This class implements Runnable and is designed for Thread-1.
o In its run() method, it calls pen.writeWithPenAndPaper(paper), which tries
to lock the Pen object first and then the Paper object.
4. Task2:
o This class implements Runnable for Thread-2.
o In its run() method, it synchronizes on the pen object first and then calls
paper.writeWithPaperAndPen(pen), which tries to lock the Paper object first
and then the Pen object.
Main Method
5. Main Method:
o Creates instances of Pen and Paper.
o Starts two threads (Thread-1 and Thread-2) that execute Task1 and Task2,
respectively.
Deadlock Scenario
Here's how a deadlock can occur with this setup:
1. Thread-1 executes pen.writeWithPenAndPaper(paper), acquiring the lock on the
Pen object.
2. Thread-2 executes synchronized (pen) (acquiring the lock on pen) and then calls
paper.writeWithPaperAndPen(pen), which attempts to acquire the lock on the
Paper object.
3. Thread-1 is now blocked, waiting to acquire the lock on Paper when it calls
paper.finishWriting().
4. Thread-2 is also blocked, waiting to acquire the lock on Pen when it calls
pen.finishWriting() inside paper.writeWithPaperAndPen(pen).
Result
Both threads are waiting indefinitely for each other to release their locks, leading to a
deadlock situation.
Summary
• The code illustrates a deadlock condition caused by two threads trying to acquire
locks on two resources (Pen and Paper) in different orders.
• To avoid such deadlocks, you could enforce a consistent locking order, use timeout
mechanisms, or utilize higher-level abstractions like java.util.concurrent constructs.
1. package MultiThreading.O3_deadlock;
@Override
public void run() {
synchronized (pen) {
paper.writeWithPaperAndPen(pen); // thread2 locks paper and tries to lock
pen
}
}
}
By declaring the Pen as synchronized, The synchronized (pen) block means that Task2 will
acquire the lock on the pen object before executing the code inside the block.
And if the lock is already aquired by Task1, in this situation the Task2 will wait untill the lock
on Pen is released by Task1 since Pen is synchronized.
Thread Communication:
Thread communication refers to the methods and mechanisms that allow threads to
communicate and coordinate their actions in a multithreaded environment. This is crucial in
ensuring that threads can work together efficiently, share data, and synchronize their
operations without running into issues like data inconsistency or race conditions.
Wait and Notify Mechanism:
• In Java, threads can communicate using the wait(), notify(), and notifyAll() methods:
o wait(): A thread can call this method on an object to release the lock and wait
until another thread invokes notify() or notifyAll() on the same object.
o notify(): Wakes up a single thread that is waiting on the object’s monitor (if
there are any).
o notifyAll(): Wakes up all threads that are waiting on the object’s monitor.
Producer-Consumer Problem:
• A classic example of thread communication is the producer-consumer problem,
where one thread (the producer) generates data and another thread (the consumer)
processes that data. Proper communication mechanisms are needed to ensure the
consumer waits for data to be available and the producer waits if storage is full.
• SharedResource class
• package MultiThreading.O4_ThreadCommunication;
while(available){
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
data = value;
System.out.println("Produced " + value);
available = true;
notify(); // Notify consumers that data is available
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
resource.produce(i);
}
}
}
• Consumer Class
• package MultiThreading.O4_ThreadCommunication;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
int value = resource.consume();
}
}
}
• Main Class
• package MultiThreading.O4_ThreadCommunication;
Thread Safety
Thread safety refers to the property of a piece of code or a data structure that guarantees
safe execution by multiple threads at the same time. In other words, a thread-safe
component can be accessed by multiple threads without leading to data corruption or
inconsistency.
Key Concepts:
1. Mutual Exclusion: Ensures that only one thread can access a resource or critical
section at a time. This is often implemented using synchronization mechanisms like
locks, semaphores, or monitors.
2. Atomic Operations: Operations that are completed in a single step from the
perspective of other threads. For example, incrementing a variable might not be
atomic if it involves reading, modifying, and writing back to memory.
3. Visibility: Changes made by one thread should be visible to others. This often
requires using memory barriers or volatile variables to ensure that the latest value is
read.
4. Deadlocks: A situation where two or more threads are blocked forever, each waiting
for the other to release a resource. Designing thread-safe code involves avoiding
conditions that can lead to deadlocks.
5. Race Conditions: Occur when two or more threads access shared data and try to
change it simultaneously, leading to unpredictable results. Thread safety mechanisms
help prevent race conditions.
Common Practices for Ensuring Thread Safety:
• Synchronization: Use synchronized blocks or methods to restrict access to critical
sections.
• Locks: Use explicit locks (like ReentrantLock in Java) to manage access to shared
resources.
• Thread-safe Collections: Use built-in thread-safe data structures (like
ConcurrentHashMap in Java).
• Immutable Objects: Design objects that cannot be changed after they are created,
making them inherently thread-safe.
Example:
In a multithreaded application, if multiple threads try to increment a shared counter,
without proper synchronization, the counter may not reflect the correct value. Using
synchronization ensures that only one thread can modify the counter at a time, maintaining
its integrity.
In summary, thread safety is crucial for building robust and reliable multithreaded
applications, ensuring that shared resources are accessed in a safe and predictable manner.
Lambda Expression
Lambda expressions in Java, introduced in Java 8, provide a concise way to represent
anonymous functions (or functional interfaces) using an expression. They are particularly
useful for passing behavior as parameters, making code more readable and reducing
boilerplate code.
Syntax
The basic syntax of a lambda expression is:
Components
1. Parameters: The input parameters to the function. You can specify types or omit
them (type inference).
2. Arrow Token (->): Separates the parameters from the body.
3. Body: The implementation of the function.
Example Usage
Here are some common use cases for lambda expressions:
1. With Functional Interfaces: Java has many built-in functional interfaces in the
java.util.function package, such as Consumer, Function, Predicate, and Supplier.
1. Simple Example with a Functional Interface
import java.util.function.Consumer;
import java.util.Arrays;
import java.util.List;
Therefore, we can use this lambda expression for Runnable Interface as well because
Runnable interface is functional Interface.
1. package MultiThreading.O5_lambdaExpression;
Since the Runnable Interface is a functional Interface which contains only one method that
is run() method. Therefore, we can use Lambda expression here to implement the Runnable
Interface.
package MultiThreading.O5_lambdaExpression;
t1.start();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long result = 1;
Here In this program, In each iteration of first for loop the fact() method is called and
it takes 1 sec for thread sleep and the factorial is calculated and the value is returned.
Now I want to create multiple threads for the calculation of this factorial program.
package MultiThreading.O6_ThreadPooling.ThreadExecutorFramework;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long result = 1;
Here In this case, In each iteration of the loop a new thread is created for the execution of
fact() method.
These threads are responsible for the parrallel execution of the fact() method.
Key Points on Parallel Execution in Your Scenario
1. Thread Creation:
o In each iteration of the loop, a new thread is created and started. However,
just because the threads are started one after another in quick succession
doesn’t mean they run sequentially.
2. Context of Execution:
o When t1.start() is called, the thread is placed in the "runnable" state, and the
operating system’s scheduler decides when to execute it. This means that
even if the threads are created one by one, the OS can switch between
threads.
3. Operating System Scheduler:
o The operating system handles the scheduling of threads. Once a thread is
started, it becomes eligible for execution, and the OS may run it immediately,
depending on its scheduling algorithm.
o If there are available CPU cores, the OS can execute multiple threads at the
same time, even if they were created in sequence.
4. Non-blocking Behavior:
o The start() method is non-blocking. This means that when you call t1.start(),
the main program continues executing without waiting for that thread to
finish. This allows the next iteration of the loop to create and start the next
thread almost immediately.
Example of Parallel Execution in Your Loop
• First Iteration:
o A thread for calculating the factorial of 1 is created and started.
• Second Iteration:
o Before the first thread has finished executing, a new thread for calculating the
factorial of 2 is created and started.
• Execution:
o If your CPU has multiple cores (e.g., 4 cores), both threads may run
simultaneously on different cores.
o If your CPU has only one core, the OS will rapidly switch between the threads,
giving the appearance of parallelism, although true parallel execution
(simultaneous execution on multiple cores) won't occur.
Summary
To achieve true parallel execution in your scenario:
• You start multiple threads in quick succession.
• The OS schedules these threads for execution.
• As soon as a thread is eligible to run, it can be executed independently of when it
was created.
Even if the creation of threads happens one after another, their execution can be parallel,
provided the system resources allow it. The key is that once a thread starts, it runs
concurrently with others, leveraging the operating system’s capabilities to manage and
execute multiple threads simultaneously.
Here we created a array of threads of size 9 and we are waiting for all the threads to
complete the execution using join() method then we will carry on the main thread to print
the total time taken.
Since here in this program we are using 9 threads for the execution of fact() method
therefore the time taken for execution will be short.
While using the multiThreads, we incountered with some issues:
1. Thread Management Overhead: Creating and destroying threads frequently can be
resource-intensive and slow down application performance.
2. Complexity of Concurrent Programming: Writing multithreaded code can be
complex and error-prone, leading to issues like race conditions, deadlocks, and
resource contention.
3. Lack of Scalability: Managing a large number of threads manually can lead to
inefficiencies and make it difficult to scale applications effectively.
4. Difficulty in Task Scheduling: Implementing scheduling for recurring or delayed tasks
manually can be cumbersome and error-prone.
5. Error Handling Challenges: Handling exceptions and errors in multithreaded
environments can be complex, leading to unhandled exceptions or inconsistent
states.
6. Inefficient Resource Utilization: Without proper management, threads can become
idle or starved for resources, leading to wasted CPU cycles and performance
degradation.
7. Blocking Operations: Managing blocking operations (like I/O) manually can lead to
poor responsiveness in applications, especially in user interfaces.
Thread Pooling
Thread pooling is a technique used in concurrent programming to manage a group of worker
threads efficiently. Instead of creating a new thread for each task, a fixed number of threads
are created and reused for executing multiple tasks, which helps reduce the overhead
associated with thread creation and destruction.
Key Concepts
1. Thread Pool: A collection of threads that are created once and reused for executing
tasks. This avoids the cost of creating a new thread for every task, which can be
significant in a high-throughput application.
2. Task Submission: Tasks can be submitted to the thread pool, typically through an
interface or method that accepts a Runnable or Callable task.
3. Work Distribution: The thread pool manages the distribution of tasks to the available
threads, ensuring that they are executed concurrently.
4. Thread Reuse: Once a thread completes its task, it returns to the pool and waits for
new tasks to be assigned, allowing for efficient use of system resources.
Benefits of Thread Pooling
• Performance Improvement: Reduces the overhead of thread creation and
destruction, leading to better performance in applications that require high
concurrency.
• Resource Management: Controls the number of concurrent threads, which can help
prevent resource exhaustion and improve stability.
• Simplified Task Management: Allows easy management of tasks without dealing
with the complexities of thread lifecycle management.
Java Implementation
In Java, the ExecutorService interface provides a simple way to create and manage thread
pools. The Executors class provides factory methods to create different types of thread
pools.
Example
Here’s a simple example of using a thread pool in Java:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
2. Submitting Tasks:
Here, we submit 10 tasks to the pool. Each task prints its ID and simulates work by sleeping
for 1 second.
3. Shutting Down the Executor:
executorService.shutdown();
Conclusion
Thread pooling is a powerful technique in concurrent programming that enhances
performance, resource management, and task handling in multithreaded applications. By
reusing a fixed number of threads, it reduces the overhead of thread management and helps
maintain a stable and efficient application.
Future
Future is an interface that represents the result of an asynchronous computation. It acts as
a placeholder for a value that may not yet be available but will be at some point in the
future.
Here are the key features and functionalities of the Future interface:
1. Asynchronous Result: A Future allows you to retrieve the result of a computation
that is performed in a separate thread, enabling non-blocking behavior.
2. Completion Status: It provides methods to check if the computation is complete,
cancelled, or still running. This helps in managing task execution more effectively.
3. Retrieving Results: You can call get() on a Future object to retrieve the result of the
computation. If the computation is not complete, this method will block until the
result is available.
4. Cancellation: The Future interface includes methods like cancel() to attempt to
cancel the execution of the task. If the task has not started yet, it will be cancelled; if
it’s already running, you can check if it was cancelled.
5. Exception Handling: If the computation throws an exception, calling get() will throw
an ExecutionException, allowing you to handle errors that occurred during the task
execution.
Example Usage
Here's a simple example of using Future with ExecutorService:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
Summary
In summary, a Future in Java provides a way to manage asynchronous computations,
allowing you to check the status of tasks, retrieve results, and handle exceptions effectively.
It plays a crucial role in concurrent programming, making it easier to work with threads and
manage the results of their computations.
Long result = future.get(); // This will block until the result is available
If the computation takes a couple of seconds, the main thread will wait during this time. If
you want to perform other tasks while waiting for the result, you would need to manage
those tasks in separate threads or use non-blocking techniques.
what is the advantage of creating a new thread even that code which is going to be
executed by genereted thread can also be executed by main thread?
While the main thread can execute the same code, using a separate thread for execution still
offers several key advantages:
1. Responsiveness: By offloading tasks to a separate thread, the main thread can
continue to execute other code or respond to user input. This is crucial in
applications with a user interface, where blocking the main thread can lead to a
frozen or unresponsive application.
2. Parallelism: On multi-core systems, separate threads can run in parallel. This means
that if your task is CPU-intensive, it can utilize multiple cores, speeding up execution
time significantly compared to running everything sequentially on the main thread.
3. Non-blocking Operations: If a task involves I/O operations (like reading from a file or
making a network request), running it in a separate thread allows the main thread to
remain free for other tasks. This prevents the application from becoming
unresponsive while waiting for the I/O operation to complete.
4. Separation of Logic: Using separate threads helps keep different concerns distinct.
For instance, one thread can handle background processing, while another handles
user interface updates or input. This separation can lead to cleaner, more
maintainable code.
5. Task Management: Utilizing a thread pool (via ExecutorService) allows for efficient
management of multiple tasks, including reusing threads and handling exceptions
more gracefully without complicating the main thread's logic.
Summary
In summary, while the main thread could execute the same tasks, using separate threads
allows for greater responsiveness, improved performance through parallelism, and better
organization of code, especially in applications where maintaining a responsive user
experience is crucial.
What is the difference between the Runnable and Callable Interfaces in java?
The main differences between the Runnable and Callable interfaces in Java are as follows:
1. Return Type
• Runnable: The run() method does not return a value. It has a void return type.
• Callable: The call() method returns a value and can throw checked exceptions. You
define the return type when implementing Callable.
2. Exception Handling
• Runnable: The run() method cannot throw checked exceptions. Any exception that
occurs must be unchecked (i.e., a subclass of RuntimeException).
• Callable: The call() method can throw checked exceptions, allowing for more flexible
error handling.
3. Usage with Executor Framework
• Runnable: You can submit a Runnable to an ExecutorService, but it cannot provide a
result.
• Callable: You can submit a Callable to an ExecutorService, and it returns a Future
object that can be used to retrieve the result once the computation is complete.
4. Purpose
• Runnable: Typically used for tasks that do not require a return value, such as
background operations or tasks that just perform actions.
• Callable: Used for tasks that need to return a result or may throw exceptions, such as
computations or tasks requiring result collection.
Example
Here’s a simple comparison:
Runnable Example:
new Thread(runnableTask).start();
Callable Example:
try {
Integer result = future.get(); // Blocking until the result is available
System.out.println("Result from Callable: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
Summary
In summary, Runnable is suitable for tasks that do not require a return value and cannot
throw checked exceptions, while Callable is better for tasks that return a result and may
throw checked exceptions, making it more flexible for complex task handling in concurrent
programming.
Methods of Executor Service
1. submit(Runnable task)
• Description: Submits a Runnable task for execution and returns a Future<?>.
• Example:
2. submit(Callable<T> task)
• Description: Submits a Callable task and returns a Future<T>.
• Example:
4. shutdown()
• Description: Initiates an orderly shutdown of the executor service, preventing new
tasks from being accepted.
• Example:
5. shutdownNow()
• Description: Attempts to stop all actively executing tasks and halts the processing of
waiting tasks.
• Example:
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // Force shutdown after timeout
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
7. isShutdown()
• Description: Returns true if the executor has been shut down.
• Example:
if (executor.isShutdown()) {
System.out.println("Executor is shut down.");
}
8. isTerminated()
• Description: Returns true if all tasks have completed following a shutdown.
• Example:
if (executor.isTerminated()) {
System.out.println("All tasks are completed.");
}
Summary
These methods provide robust mechanisms for managing concurrent tasks in Java, allowing
you to control task execution, shutdown behavior, and manage results effectively.
Example
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
// Shutdown
executor.shutdown();
// Check if shutdown
if (executor.isShutdown()) {
System.out.println("Executor is shut down.");
}
// Invoke any
Integer firstResult = executor.invokeAny(Arrays.asList(
() -> { Thread.sleep(1000); return 1; },
() -> { return 2; }
));
System.out.println("First completed task result: " + firstResult);
// Await termination
executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
}
}
Methods on Future
The Future interface in Java represents the result of an asynchronous computation. Here are
the key methods provided by the Future interface, along with brief explanations and
examples:
1. get()
• Description: Retrieves the result of the computation when it is complete. This
method blocks until the result is available.
• Example:
3. isDone()
• Description: Returns true if the task is completed, regardless of whether it
completed successfully, failed, or was canceled.
• Example:
if (future.isDone()) {
System.out.println("Task is completed.");
} else {
System.out.println("Task is still running.");
}
4. isCancelled()
• Description: Returns true if the task was canceled before it completed.
• Example:
5. cancel(boolean mayInterruptIfRunning)
• Description: Attempts to cancel the task. If the task is already running, it can be
interrupted if mayInterruptIfRunning is true.
• Example:
Complete Example
// Final cleanup
executor.shutdown();
}
}
This example demonstrates how to use the Future interface methods to manage and
interact with asynchronous tasks effectively.
ScheduledExecutorService:
ScheduledExecutorService is an interface in Java that provides methods to schedule tasks to
be executed after a certain delay, or periodically, in a background thread pool. It is part of
the java.util.concurrent package, which allows you to handle concurrent tasks in a more
efficient and flexible way than traditional Timer or Thread classes.
Key Features of ScheduledExecutorService:
1. Scheduling with Delay: You can schedule tasks to be executed after a fixed delay.
2. Periodic Tasks: You can schedule tasks to be executed periodically with fixed-rate or
fixed-delay execution policies.
3. Thread Pool Management: It manages a pool of worker threads to execute tasks
concurrently.
4. Task Cancellation: It provides methods to cancel scheduled tasks before they
execute.
Common Methods:
• schedule(Runnable command, long delay, TimeUnit unit): Schedules a task to run
once after the specified delay.
• schedule(Callable<V> callable, long delay, TimeUnit unit): Schedules a Callable task
to run once after the specified delay.
• scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit
unit): Schedules a task to run repeatedly at a fixed rate.
• scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,
TimeUnit unit): Schedules a task to run repeatedly with a fixed delay between the
end of one execution and the start of the next.
• shutdown(): Initiates an orderly shutdown of the executor, in which previously
submitted tasks are executed, but no new tasks will be accepted.
Example Usage:
import java.util.concurrent.*;
Types of Tasks:
1. Fixed-rate Scheduling (scheduleAtFixedRate): The time between the start of one
execution and the start of the next is fixed. This may cause overlapping executions if
the task takes longer to execute than the period specified.
2. Fixed-delay Scheduling (scheduleWithFixedDelay): The time between the completion
of one execution and the start of the next is fixed. This ensures that there's a fixed
delay between executions, but the actual interval may vary depending on how long
the task takes to execute.
Choosing Between Fixed-Rate vs. Fixed-Delay:
• Use fixed-rate when you want the tasks to execute at consistent intervals regardless
of how long the task execution takes.
• Use fixed-delay when you want the task to be executed after the previous task has
completed, ensuring a delay between each execution.
Important Notes:
• Always ensure to shut down the executor using shutdown() to prevent resource
leakage.
• Be cautious of long-running tasks in the executor, as they could block the thread
pool.
Alternatives to ScheduledExecutorService:
• Timer: An older approach, but less flexible and less robust than
ScheduledExecutorService.
• CompletableFuture: Can also be used for scheduling tasks with delays, especially
when you want to work with future results.
detailed example showing both fixed-rate scheduling and fixed-delay scheduling using the
ScheduledExecutorService:
Example Code:
import java.util.concurrent.*;
// Fixed-rate scheduling (time between the start of one execution and the start
of the next is fixed)
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Fixed-rate task started at: " + System.currentTimeMillis());
try {
// Simulate a task that takes time to execute
Thread.sleep(3000); // sleep for 3 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Fixed-rate task completed at: " +
System.currentTimeMillis());
}, 0, 2, TimeUnit.SECONDS); // Initial delay: 0, Period: 2 seconds
Key Differences:
• Fixed-rate: The tasks are scheduled to start at a fixed interval (2 seconds here). If a
task takes longer than the scheduled period, it can overlap with the next task.
• Fixed-delay: The tasks are scheduled with a delay between the end of one execution
and the start of the next. This avoids overlap but results in irregular intervals if tasks
take longer than expected.
Expected Output (Example Timing):
The Fixed-rate task will execute at a constant rate, potentially leading to overlap, while the
Fixed-delay task respects the execution time, ensuring that tasks don't overlap and are
spaced out by the specified delay.