ADV JAVA
Threads and Multithreading
Threads and multithreading are fundamental concepts in programming that allow for
concurrent execution of tasks, improving the efficiency and performance of applications.
Here’s a detailed overview of both concepts:
What is a Thread?
A thread is the smallest unit of processing that can be scheduled by an operating system. It
is a lightweight process that shares the same memory space with other threads within the
same process. Threads allow for parallel execution of tasks, enabling applications to perform
multiple operations simultaneously.
Key Characteristics of Threads:
Lightweight: Threads are more lightweight than processes, as they share the same
memory space and resources.
Shared Memory: Threads within the same process can access shared data and
resources, which facilitates communication but also requires synchronization to avoid
conflicts.
Concurrency: Threads enable concurrent execution, allowing multiple tasks to be
performed at the same time.
What is Multithreading?
Multithreading is the ability of a CPU (or a single core in a multi-core processor) to provide
multiple threads of execution concurrently. It allows a program to perform multiple
operations simultaneously, improving the overall performance and responsiveness of
applications.
Advantages of Multithreading:
1. Improved Performance: Multithreading can lead to better CPU utilization and faster
execution of programs, especially for I/O-bound and CPU-bound tasks.
2. Responsiveness: Applications can remain responsive to user input while performing
background tasks (e.g., loading data).
3. Resource Sharing: Threads share the same memory space, which makes it easier to
share data and resources between them.
4. Simplified Program Structure: Multithreading can simplify the design of programs
that require concurrent operations.
Thread Lifecycle
A thread in Java can be in one of several states during its lifecycle:
New: The thread is created but not yet started.
Runnable: The thread is ready to run and waiting for CPU time.
Blocked: The thread is blocked waiting for a monitor lock.
Waiting: The thread is waiting indefinitely for another thread to perform a particular
action.
Timed Waiting: The thread is waiting for another thread to perform an action for a
specified waiting time.
Terminated: The thread has completed its execution.
Synchronization
When multiple threads access shared resources, synchronization is necessary to prevent
data inconsistency. Java provides several mechanisms for synchronization, including:
Synchronized Methods: Methods can be declared as synchronized to ensure that
only one thread can execute them at a time.
Synchronized Blocks: Specific blocks of code can be synchronized to control access to
shared resources.
Feature Process
Definition A process is an independent program in execution, with its own memory space and resource
Memory Each process has its own memory space, including code, data, and stack segments.
Overhead Higher overhead due to separate memory allocation and management.
Inter-process communication (IPC) is required for processes to communicate, which can be
Communication
slower.
Creation Creating a process is more time-consuming and resource-intensive.
Execution Processes are executed independently and do not affect each other.
Isolation Processes are isolated from each other, providing better security and stability.
Context
Context switching between processes is more expensive due to the need to switch memory
Switching
Feature Process
Suitable for applications that require isolation and separate execution environments (e.g., ru
Use Cases
applications).
Java Program to Create and Run Two Threads Concurrently
class MyThread extends Thread {
private String threadName;
MyThread(String name) {
this.threadName = name;
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + " - Count: " + i);
try {
// Sleep for 100 milliseconds to simulate work
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted.");
}
}
System.out.println(threadName + " has finished execution.");
}
}
public class ThreadExample {
public static void main(String[] args) {
// Create two threads
MyThread thread1 = new MyThread("Thread 1");
MyThread thread2 = new MyThread("Thread 2");
// Start both threads
thread1.start();
thread2.start();
// Optionally, wait for both threads to finish
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread has finished execution.");
}
}
Synchronizing Threads in Java
1. Synchronized Methods: You can declare a method as synchronized by using
the synchronized keyword. This ensures that only one thread can execute that
method on the same object at a time.
2. Synchronized Blocks: You can also synchronize a block of code within a method. This
allows for more fine-grained control over synchronization, as you can specify the
object to lock on.
Purpose of wait() and notify()
1. wait() Method:
Purpose: The wait() method causes the current thread to release the monitor
(lock) it holds on the object and enter a waiting state. The thread will remain
in this state until another thread invokes notify() or notifyAll() on the same
object.
Use Case: It is typically used when a thread needs to wait for a condition to
be met before it can proceed. For example, a producer thread may
call wait() if the buffer is full, and it will wait until a consumer thread notifies
it that space is available.
2. notify() Method:
Purpose: The notify() method wakes up a single thread that is waiting on the
object's monitor. If multiple threads are waiting, one of them is chosen at the
discretion of the thread scheduler.
Use Case: It is used to signal a waiting thread that a condition has changed,
allowing it to proceed. For example, a consumer thread may call notify() after
it has consumed an item from a buffer, indicating to the producer that it can
now produce more items.
3. notifyAll() Method:
Purpose: Similar to notify(), but it wakes up all threads that are waiting on
the object's monitor.
Use Case: It is used when multiple threads are waiting for a condition, and
you want to notify all of them to check the condition again.
1. Program to Use Thread.join()
The join() method allows one thread to wait for the completion of another thread. This is
useful when you want to ensure that a thread has finished executing before proceeding with
the next steps in your program.
class MyThread extends Thread {
private String threadName;
MyThread(String name) {
this.threadName = name;
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + " - Count: " + i);
try {
Thread.sleep(100); // Sleep for 100 milliseconds
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted.");
}
}
System.out.println(threadName + " has finished execution.");
}
}
public class JoinExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread("Thread 1");
MyThread thread2 = new MyThread("Thread 2");
thread1.start();
thread2.start();
try {
// Wait for thread1 to finish
thread1.join();
System.out.println("Thread 1 has finished.");
// Wait for thread2 to finish
thread2.join();
System.out.println("Thread 2 has finished.");
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread has finished execution.");
}
}
2. Program to Use the Runnable Interface
The Runnable interface is a functional interface that represents a task that can be executed
by a thread. It is a more flexible way to create threads compared to extending
the Thread class.
class MyRunnable implements Runnable {
private String threadName;
MyRunnable(String name) {
this.threadName = name;
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + " - Count: " + i);
try {
Thread.sleep(100); // Sleep for 100 milliseconds
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted.");
}
}
System.out.println(threadName + " has finished execution.");
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable1 = new MyRunnable("Runnable Thread 1");
MyRunnable myRunnable2 = new MyRunnable("Runnable Thread 2");
Thread thread1 = new Thread(myRunnable1);
Thread thread2 = new Thread(myRunnable2);
thread1.start();
thread2.start();
try {
// Wait for thread1 to finish
thread1.join();
System.out.println("Runnable Thread 1 has finished.");
// Wait for thread2 to finish
thread2.join();
System.out.println("Runnable Thread 2 has finished.");
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread has finished execution.");
}
}
How to Handle Thread Interruptions
1. Interrupting a Thread: You can interrupt a thread by calling its interrupt() method.
2. Checking for Interruptions: A thread can check if it has been interrupted using
the isInterrupted() method or by catching the InterruptedException when it is in a
blocking state (e.g., sleeping or waiting).
3. Graceful Termination: When a thread detects that it has been interrupted, it should
perform any necessary cleanup and terminate gracefully.
The Thread.sleep() and Thread.yield() methods in Java are both used to control thread
execution, but they serve different purposes and have different effects on thread scheduling.
Here’s a detailed comparison of the two methods, along with examples.
1. Thread.sleep()
Purpose: The sleep() method is used to pause the execution of the current thread for
a specified period (in milliseconds). During this time, the thread is not eligible to run,
and it will not consume CPU resources.
Effect: When a thread calls sleep(), it goes into a "sleeping" state, and the thread
scheduler will not schedule it for execution until the specified time has elapsed.
Use Case: It is commonly used to simulate delays, control the timing of thread
execution, or allow other threads to run.
2. Thread.yield()
Purpose: The yield() method is used to suggest to the thread scheduler that the
current thread is willing to yield its current use of the CPU. It allows other threads of
the same priority to run.
Effect: When a thread calls yield(), it does not go to sleep; instead, it moves to the
runnable state, allowing other threads to execute. However, it is up to the thread
scheduler to decide whether to actually switch to another thread.
Use Case: It is used to improve the responsiveness of applications by allowing other
threads to run, especially in a multi-threaded environment where threads have the
same priority.
Feature Thread.sleep() Thread.yield()
Purpose Pauses the current thread for a specified time. Suggests to the thre
State Change Moves the thread to the "sleeping" state. Moves the thread b
CPU Usage The thread does not consume CPU resources while sleeping. The thread may still
Time Control Allows precise control over timing. Does not guarantee
Feature Thread.sleep() Thread.yield()
Use Case Used for creating delays or simulating time-consuming tasks. Used to improve res