Multithreading in Java
Introduction to Threads
Theory:
● A thread in Java is a lightweight process. Each thread is a separate path of
execution within a program.
● Threads allow a program to perform multiple operations concurrently, improving the
efficiency and responsiveness of applications.
● Java provides built-in support for multithreading through the Thread class and the
Runnable interface.
● By default, Java applications have a single thread of execution, known as the main
thread.
How Threads Work:
● A Thread in Java can be created in two ways:
1. By extending the Thread class.
2. By implementing the Runnable interface.
Example 1: Using Thread Class
class MyThread extends Thread {
public void run() {
[Link]([Link]().getId() + " is
running.");
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
[Link](); // Start first thread
[Link](); // Start second thread
}
}
Output:
1 is running.
2 is running.
Explanation:
● Two threads are created, and the start() method is called to begin their execution.
Each thread prints its unique ID.
Multithreading Concepts
Theory:
● Multithreading allows multiple threads to execute independently but share the same
resources, such as memory and CPU time.
● It is used to perform tasks in parallel, such as downloading a file and processing the
file simultaneously.
● Multithreading helps to maximize CPU usage and improve the performance of
applications that need to perform many tasks simultaneously.
Types of Threads:
1. User Threads: These threads are created by the application. When the main thread
ends, all user threads are terminated.
2. Daemon Threads: These are background threads, such as garbage collection,
which run in the background without interrupting the program's main execution.
Example 2: Using Runnable Interface
class Task implements Runnable {
public void run() {
[Link]([Link]().getId() + " is
performing task.");
}
public static void main(String[] args) {
Task task = new Task();
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
[Link](); // Start first thread
[Link](); // Start second thread
}
}
Output:
1 is performing task.
2 is performing task.
Explanation:
● In this example, the Task class implements the Runnable interface, which defines
the run() method.
● A Thread object is created with the Task instance as the target and started using
the start() method.
Stages of the Thread Life Cycle
A thread in Java can be in one of the following states:
1. New (Born)
2. Runnable (Ready to Run)
3. Blocked/Waiting (Not Running)
4. Timed Waiting
5. Terminated (Dead)
1. New (Born)
● When a thread is created using the Thread class or implementing Runnable, it is in
the New state.
● At this stage, the thread is not yet started.
Code Example:
Thread t = new Thread();
[Link]("Thread state after creation: " + [Link]());
// NEW
2. Runnable (Ready to Run)
● A thread is in the Runnable state after calling the start() method on it.
● At this point, the thread is ready to be executed by the CPU, but it might not be
running immediately due to CPU scheduling.
Code Example:
Thread t = new Thread(() -> {
[Link]("Thread is running.");
});
[Link](); // Now the thread is in the RUNNABLE state
3. Blocked / Waiting
● A thread enters the Blocked state when it cannot proceed with its execution because
it's waiting to acquire a resource that is currently held by another thread.
● A thread enters the Waiting state when it’s waiting for another thread to perform a
specific action to resume its work.
Code Example (Blocked):
class BlockedStateExample {
synchronized void method1() {
try {
[Link](2000); // Simulate some work
} catch (InterruptedException e) {
[Link]();
synchronized void method2() {
[Link]("Inside method2.");
}
public class Main {
public static void main(String[] args) {
BlockedStateExample example = new BlockedStateExample();
Thread t1 = new Thread(() -> example.method1()); // t1 will
acquire the lock
Thread t2 = new Thread(() -> example.method2()); // t2 will
be blocked waiting for the lock
[Link](); // Starts method1(), thread t1 will not be
blocked, it will acquire the lock
[Link](); // Starts method2(), thread t2 will be blocked
until t1 finishes
// The second thread (t2) will remain in the BLOCKED state
until the first thread (t1) completes its task.
4. Timed Waiting
● A thread enters the Timed Waiting state when it is waiting for a specific period
(using methods like sleep(), join(), etc.).
● Once the specified time is over, the thread transitions back to the Runnable state.
Code Example (Timed Waiting):
class TimedWaitingExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
[Link]("Thread is going to sleep for 2
seconds.");
[Link](2000); // The thread will be in the
TIMED_WAITING state for 2 seconds.
[Link]("Thread woke up after 2
seconds.");
} catch (InterruptedException e) {
[Link]();
});
[Link]();
In this example, the thread goes into the Timed Waiting state because of
[Link](2000).
5. Terminated (Dead)
● A thread enters the Terminated state after it has completed its execution or if it is
terminated due to an exception.
● Once in the Terminated state, the thread cannot be restarted.
Code Example:
class TerminatedStateExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {
[Link]("Thread is running.");
});
[Link](); // The thread starts executing
// Once the thread finishes executing, it will be in the
TERMINATED state.
try {
[Link](); // Wait for thread to finish
} catch (InterruptedException e) {
[Link]();
[Link]("Thread state after completion: " +
[Link]()); // TERMINATED
}
Thread Life Cycle Diagram:
New -> Runnable -> Running -> (Blocked / Waiting) -> Timed Waiting
-> Terminated
1. New: Thread is created but not started.
2. Runnable: Thread is ready to be executed.
3. Running: The thread is executing.
4. Blocked / Waiting: Thread is waiting for some resource or condition.
5. Timed Waiting: Thread is waiting for a specific amount of time.
6. Terminated: Thread has completed execution.
Real-Life Scenario: Thread Life Cycle
Let's consider a multi-threaded order processing system for an e-commerce website.
1. New (Born):
○ Each order placed on the website creates a new thread to process the
payment, update the inventory, and send a confirmation email.
2. Runnable:
○ The thread is ready to be processed by the CPU. All resources are available
for the thread to begin executing the task.
3. Blocked/Waiting:
○ If the thread is waiting for external resources, such as network access or
database updates, it enters the Blocked or Waiting state. For instance, the
thread might be blocked while waiting for a database connection to process
the payment.
4. Timed Waiting:
○ If the thread is waiting for a response from the payment gateway, it may enter
the Timed Waiting state for a specific period before retrying or timing out.
5. Terminated:
○ Once the order has been successfully processed, the thread finishes
execution and enters the Terminated state.
Key Points to Remember:
● Thread Scheduling: The JVM is responsible for managing thread states and
determining when a thread moves from one state to another based on the system's
scheduling algorithm.
● Thread Coordination: Threads in Java can communicate and synchronize their
actions using techniques such as wait(), notify(), and join().
● Thread Management: You can use thread pools to manage multiple threads, which
avoids the overhead of creating and destroying threads repeatedly.
How Thread Synchronization Works
Thread synchronization ensures that multiple threads do not interfere with each other
when accessing shared resources. It prevents race conditions by ensuring that only one
thread can access the critical section (shared resource) at a time.
Key Concepts in Synchronization
1. Critical Section: The part of code where shared resources are accessed.
2. Monitor: A synchronization construct (intrinsic lock) associated with each Java
object. Only one thread can own the monitor at a time.
3. Synchronized Keyword: Used to lock the critical section or method, ensuring that
only one thread executes it at a time.
Example Without Synchronization
Scenario: A bank account has a balance, and multiple threads (representing users) are
trying to withdraw money at the same time. Without synchronization, two users might
withdraw at the same time, leading to incorrect results.
Code Example:
class BankAccount {
private int balance = 100;
public void withdraw(int amount) {
if (balance >= amount) {
[Link]([Link]().getName() + "
is withdrawing " + amount);
balance -= amount;
[Link]([Link]().getName() + "
completed withdrawal. Remaining balance: " + balance);
} else {
[Link]([Link]().getName() + "
tried to withdraw " + amount + " but insufficient funds.");
}
public class WithoutSyncExample {
public static void main(String[] args) {
BankAccount account = new BankAccount();
Runnable withdrawTask = () -> [Link](70);
Thread user1 = new Thread(withdrawTask, "User 1");
Thread user2 = new Thread(withdrawTask, "User 2");
[Link]();
[Link]();
Expected Output Without Synchronization (varies due to thread scheduling):
User 1 is withdrawing 70
User 2 is withdrawing 70
User 1 completed withdrawal. Remaining balance: 30
User 2 completed withdrawal. Remaining balance: -40
Problem:
● Both threads accessed the balance concurrently and withdrew money without
checking the latest balance, leading to an overdraft.
Example With Synchronization
Solution: Use the synchronized keyword to prevent multiple threads from executing the
withdraw method simultaneously.
Code Example:
class BankAccount {
private int balance = 100;
public synchronized void withdraw(int amount) {
if (balance >= amount) {
[Link]([Link]().getName() + "
is withdrawing " + amount);
balance -= amount;
[Link]([Link]().getName() + "
completed withdrawal. Remaining balance: " + balance);
} else {
[Link]([Link]().getName() + "
tried to withdraw " + amount + " but insufficient funds.");
}
public class WithSyncExample {
public static void main(String[] args) {
BankAccount account = new BankAccount();
Runnable withdrawTask = () -> [Link](70);
Thread user1 = new Thread(withdrawTask, "User 1");
Thread user2 = new Thread(withdrawTask, "User 2");
[Link]();
[Link]();
Expected Output With Synchronization:
User 1 is withdrawing 70
User 1 completed withdrawal. Remaining balance: 30
User 2 tried to withdraw 70 but insufficient funds.
Explanation:
● The synchronized keyword ensures that only one thread accesses the withdraw
method at a time.
● User 1 withdraws first, updates the balance, and releases the lock.
● User 2 then checks the updated balance and sees insufficient funds.
Step-by-Step Comparison
Without Synchronization With Synchronization
Multiple threads execute the critical section Only one thread executes the critical
concurrently. section at a time.
Leads to inconsistent state (e.g., overdraft in Prevents inconsistent state by ensuring
the example). proper balance checks.
Race condition occurs when threads access Eliminates race conditions by enforcing
shared data simultaneously. mutual exclusion.
Real-Life Scenarios
1. Banking Systems:
○ Without Sync: Two ATMs withdraw money from the same account at the
same time, causing an overdraft.
○ With Sync: Only one ATM processes the transaction, ensuring the balance is
updated before the next transaction.
2. E-commerce Inventory:
○ Without Sync: Two customers purchase the last item at the same time,
causing stock inconsistencies.
○ With Sync: The stock is decremented sequentially, ensuring only one
purchase succeeds.
Thread Priorities in Java
Thread priorities are a way to influence the thread scheduler to determine the order in
which threads are executed. Each thread is assigned a priority value between 1 (lowest) and
10 (highest) using constants from the Thread class:
● Thread.MIN_PRIORITY = 1
● Thread.NORM_PRIORITY = 5 (default)
● Thread.MAX_PRIORITY = 10
However, thread priority doesn't guarantee the order of execution—it simply suggests which
threads are "more important." The actual execution depends on the thread scheduler of the
operating system.
How Thread Priority Works
1. Priority Value: Threads with higher priorities are more likely to run before threads
with lower priorities.
2. Thread Scheduler: Java delegates thread scheduling to the OS, so behavior might
vary across platforms.
3. Time-Slice Sharing: On time-sharing systems, lower-priority threads may still
execute when higher-priority threads are waiting or blocked.
Code Example: Thread Priorities
Without Priority Adjustment:
class MyThread extends Thread {
public void run() {
[Link]([Link]().getName() + " with
priority " + [Link]().getPriority() + " is running.");
}
}
public class ThreadPriorityExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
[Link]("Thread-1");
[Link]("Thread-2");
[Link]("Thread-3");
// Default priority (5)
[Link]();
[Link]();
[Link]();
}
}
Output:
Thread-1 with priority 5 is running.
Thread-2 with priority 5 is running.
Thread-3 with priority 5 is running.
With Priority Adjustment:
class MyThread extends Thread {
public void run() {
[Link]([Link]().getName() + " with
priority " + [Link]().getPriority() + " is running.");
}
}
public class ThreadPriorityExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
[Link]("High Priority Thread");
[Link]("Normal Priority Thread");
[Link]("Low Priority Thread");
[Link](Thread.MAX_PRIORITY); // Priority 10
[Link](Thread.NORM_PRIORITY); // Priority 5
[Link](Thread.MIN_PRIORITY); // Priority 1
[Link]();
[Link]();
[Link]();
}
}
Output:
High Priority Thread with priority 10 is running.
Normal Priority Thread with priority 5 is running.
Low Priority Thread with priority 1 is running.
Threads with higher priority are more likely to run earlier, but it is not guaranteed.
Real-Life Use Cases
1. Background Tasks in Applications
● Example: In a video streaming app, a high-priority thread can handle video
playback while a low-priority thread handles background buffering.
Scenario:
Thread playback = new Thread(() -> [Link]("Video
playback running..."));
Thread buffering = new Thread(() -> [Link]("Buffering in
the background..."));
[Link](Thread.MAX_PRIORITY); // Ensure smooth
playback
[Link](Thread.MIN_PRIORITY); // Background task
[Link]();
[Link]();
2. Real-Time Systems
● Example: In an embedded system, high-priority threads might monitor sensors,
while low-priority threads log data periodically.
3. User Interfaces
● Example: A high-priority thread can manage user interactions, while a low-priority
thread updates UI components in the background.
How the System Handles Priorities
1. Thread Scheduler: The OS thread scheduler considers priority but may also rely on
other factors like time slicing or the number of active threads.
Conclusion
1. Thread Priorities: Suggests which threads are more important but doesn't guarantee
execution order.
2. Practical Use: Useful in systems with time-sensitive and background tasks.
3. Best Practice: Use priorities judiciously and avoid over-reliance to ensure fair and
efficient thread execution.
How to Ensure Order in Multi-threading
If you require threads to execute in a specific order, consider the following approaches:
1. Use join()
The join() method ensures that one thread finishes before the next thread starts.
public class ThreadJoinExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> [Link]("Thread-1 is
running."));
Thread t2 = new Thread(() -> [Link]("Thread-2 is
running."));
Thread t3 = new Thread(() -> [Link]("Thread-3 is
running."));
try {
[Link]();
[Link](); // Wait for t1 to finish
[Link]();
[Link](); // Wait for t2 to finish
[Link]();
[Link](); // Wait for t3 to finish
} catch (InterruptedException e) {
[Link]();
}
}
}
Output:
Thread-1 is running.
Thread-2 is running.
Thread-3 is running.
2. Thread Synchronization
If threads share resources, synchronize their access to prevent race conditions.
class SyncExample {
synchronized void printMessage(String threadName) {
[Link](threadName + " is executing.");
}
}
public class ThreadSyncExample {
public static void main(String[] args) {
SyncExample syncObj = new SyncExample();
Thread t1 = new Thread(() ->
[Link]("Thread-1"));
Thread t2 = new Thread(() ->
[Link]("Thread-2"));
Thread t3 = new Thread(() ->
[Link]("Thread-3"));
[Link]();
[Link]();
[Link]();
}
}
3. ExecutorService (Recommended)
Modern Java applications often use the ExecutorService to manage thread execution in a
controlled way.
import [Link];
import [Link];
public class ExecutorServiceExample {
public static void main(String[] args) {
ExecutorService executor = [Link](2);
[Link](() -> [Link]("Thread-1 is
running."));
[Link](() -> [Link]("Thread-2 is
running."));
[Link](() -> [Link]("Thread-3 is
running."));
[Link]();
}
}
Output: The ExecutorService handles thread execution efficiently, and the output order
is controlled by the implementation.
Conclusion:
● Order of .start() calls does not matter for thread execution order.
● To enforce order, use tools like join(), synchronization, or thread pool
management via ExecutorService.