Multithreading and Multiprocessing in Java
Java supports concurrent programming through multithreading and multiprocessing. These mechanisms enable a program to perform multiple tasks simultaneously, improving performance and resource utilization.
What is Multiprocessing?
Multiprocessing is the capability of a system to run multiple processes simultaneously. Each process runs independently, having its own memory space and resources.
Example: Running multiple Java applications on the same system.
What is Multithreading?
Multithreading is a programming technique where a single process can execute multiple threads concurrently. Threads share the same memory space and resources.
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running...");
}
}
public class MultithreadingExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // Start the thread
}
}
Differences Between Multiprocessing and Multithreading
Feature | Multiprocessing | Multithreading |
---|---|---|
Memory | Each process has its own memory space. | Threads share the same memory space. |
Communication | Processes communicate using IPC (Inter-Process Communication). | Threads communicate more easily by sharing memory. |
Overhead | Higher due to context switching between processes. | Lower due to lightweight threads. |
Execution | Independent processes run in parallel. | Threads run within the same process. |
Creating Threads in Java
Threads can be created in Java using two main approaches:
1. Extending the Thread Class
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running...");
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // Start the thread
}
}
2. Implementing the Runnable Interface
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running...");
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.start(); // Start the thread
}
}
Getting and Setting the Name of a Thread
In Java, every thread has a name, which can be set by the programmer or automatically generated by the JVM. The Thread
class provides methods to get and set the name of a thread.
Methods to Get and Set Thread Name
getName()
: Returns the current name of the thread.setName(String name)
: Sets a new name for the thread. This can only be done before the thread starts.
Example:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running.");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread(); // Create a thread instance
thread.setName("MyCustomThread"); // Set the thread's name
thread.start(); // Start the thread
System.out.println("Thread name: " + thread.getName()); // Get the thread's name
}
}
In this example, the thread's name is set before starting the thread, and the name is then retrieved and displayed.
Getting the Current Executing Thread
The Thread.currentThread()
method returns a reference to the currently executing thread.
Example:
public class Main {
public static void main(String[] args) {
// Get the reference to the currently executing thread
Thread currentThread = Thread.currentThread();
// Display the name of the current thread
System.out.println("Current thread name: " + currentThread.getName());
// You can also set a new name for the current thread
currentThread.setName("MainThread");
// Display the new name of the current thread
System.out.println("Updated thread name: " + currentThread.getName());
// Example of starting a new thread using an anonymous inner class
Thread newThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("New thread is running.");
System.out.println("New thread name: " + Thread.currentThread().getName());
}
});
newThread.start();
}
}
This example demonstrates how to get and set the name of the currently executing thread, and also how to start a new thread with an anonymous inner class.
Thread Priorities
Every thread in Java has a priority that determines the order in which threads are scheduled for execution. Thread priorities range from 1 (lowest) to 10 (highest), with the default priority being 5.
Standard Priority Constants
Thread.MIN_PRIORITY (1)
: The lowest thread priority.Thread.NORM_PRIORITY (5)
: The default thread priority.Thread.MAX_PRIORITY (10)
: The highest thread priority.
Getting and Setting Thread Priority
getPriority()
: Returns the current priority of the thread.setPriority(int newPriority)
: Sets the priority of the thread. The priority must be between 1 and 10, inclusive.
Error Handling for Invalid Priority
If an invalid priority (less than 1 or greater than 10) is set, the IllegalArgumentException
will be thrown.
Example:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running with priority " +
Thread.currentThread().getPriority());
}
}
public class Main {
public static void main(String[] args) {
// Create threads with different priorities
Thread highPriorityThread = new Thread(new MyRunnable(), "HighPriorityThread");
Thread mediumPriorityThread = new Thread(new MyRunnable(), "MediumPriorityThread");
Thread lowPriorityThread = new Thread(new MyRunnable(), "LowPriorityThread");
// Set priorities
highPriorityThread.setPriority(Thread.MAX_PRIORITY); // Priority 10
mediumPriorityThread.setPriority(Thread.NORM_PRIORITY); // Priority 5
lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // Priority 1
// Start the threads
lowPriorityThread.start();
mediumPriorityThread.start();
highPriorityThread.start();
}
}
This example demonstrates how threads with different priorities are executed. The thread with the highest priority will be favored by the thread scheduler, although the exact execution order may vary depending on the platform.
Summary
Concept | Method | Description |
---|---|---|
Thread Name | setName(String name) & getName() |
Set and get the name of a thread. |
Current Thread | currentThread() |
Get a reference to the currently executing thread. |
Thread Priority | setPriority(int newPriority) & getPriority() |
Set and get the priority of a thread. Valid priorities range from 1 to 10. |
Invalid Priority | IllegalArgumentException |
Thrown when an invalid priority value is set outside the range of 1 to 10. |
The Methods to Prevent a Thread from Execution:
We can prevent(stop) a Thread execution by using the following methods. 1. sleep(); 2. join(); 3. yield();
1. sleep() Method
The sleep()
method pauses the current thread for a specified amount of time. If the thread is interrupted while sleeping, it throws an InterruptedException
.
class SleepExample extends Thread {
public void run() {
try {
System.out.println("Thread sleeping for 2 seconds...");
Thread.sleep(2000); // Sleep for 2 seconds
System.out.println("Thread woke up!");
} catch (InterruptedException e) {
System.out.println("Thread was interrupted during sleep.");
}
}
public static void main(String[] args) {
SleepExample t1 = new SleepExample();
t1.start();
}
}
2. join() Method
The join()
method allows one thread to wait for another thread to finish its execution.
class JoinExample extends Thread {
public void run() {
System.out.println(Thread.currentThread().getName() + " is running...");
}
public static void main(String[] args) throws InterruptedException {
JoinExample t1 = new JoinExample();
JoinExample t2 = new JoinExample();
t1.start();
t2.start();
t1.join(); // Main thread waits for t1 to finish
t2.join(); // Main thread waits for t2 to finish
System.out.println("Both threads have finished.");
}
}
3. yield() Method
The yield()
method is a suggestion to the thread scheduler that the current thread is willing to yield its CPU time slice. This method does not guarantee that other threads will execute immediately.
class YieldExample extends Thread {
public void run() {
System.out.println(Thread.currentThread().getName() + " started.");
Thread.yield(); // Yield to other threads
System.out.println(Thread.currentThread().getName() + " resumed.");
}
public static void main(String[] args) {
YieldExample t1 = new YieldExample();
YieldExample t2 = new YieldExample();
t1.start();
t2.start();
}
}
Differences Between sleep()
, join()
, yield()
Method | Description | Behavior |
---|---|---|
sleep() |
Pauses the current thread for a specified time. | Thread is paused for the specified duration, can be interrupted. |
join() |
Allows the current thread to wait for another thread to complete. | Blocks the current thread until the thread it is called on finishes executing. |
yield() |
Suggests the current thread yield control to other threads of the same priority. | Thread gives up its CPU time slice, but no guarantee another thread will run. |
Interrupting Threads
Interrupting a thread allows you to stop its execution. The interrupt()
method sets the interrupt flag for a thread, but the thread itself must periodically check this flag to handle the interruption appropriately.
class InterruptExample extends Thread {
public void run() {
try {
Thread.sleep(5000); // Sleep for 5 seconds
} catch (InterruptedException e) {
System.out.println("Thread was interrupted.");
}
}
public static void main(String[] args) throws InterruptedException {
InterruptExample t1 = new InterruptExample();
t1.start();
Thread.sleep(2000); // Wait for 2 seconds before interrupting
t1.interrupt(); // Interrupt the thread
}
}
In this example, the thread is interrupted after 2 seconds, and the interrupt flag is set, causing the thread to handle the interruption.
Thread Methods
Method | Description |
---|---|
start() |
Starts the thread and calls the run() method. |
run() |
Contains the code to be executed by the thread. |
sleep() |
Pauses the thread for a specified time. |
join() |
Waits for the thread to finish execution. |
isAlive() |
Checks if the thread is still running. |
Thread Life Cycle
Each thread in Java can be in one of several states:
- New: The thread is created but not started.
- Runnable: The thread is ready to run and is waiting for the CPU.
- Blocked: The thread is blocked while waiting for resources.
- Waiting: The thread is waiting indefinitely for another thread to perform a particular action.
- Timed Waiting: The thread is waiting for a specific amount of time.
- Terminated: The thread has finished executing.
Example of Thread State Transitions:
class ThreadStateExample extends Thread {
public void run() {
System.out.println("Thread is running...");
}
public static void main(String[] args) throws InterruptedException {
ThreadStateExample t1 = new ThreadStateExample();
System.out.println("State after creation: " + t1.getState()); // NEW
t1.start();
System.out.println("State after calling start(): " + t1.getState()); // RUNNABLE
t1.join();
System.out.println("State after thread termination: " + t1.getState()); // TERMINATED
}
}
Daemon Threads
Daemon threads run in the background to perform tasks like garbage collection. Use the setDaemon()
method to mark a thread as a daemon.
class MyDaemonThread extends Thread {
public void run() {
System.out.println("Daemon thread running...");
}
}
public class DaemonExample {
public static void main(String[] args) {
MyDaemonThread t1 = new MyDaemonThread();
t1.setDaemon(true); // Set as daemon thread
t1.start();
}
}
Synchronization in Java
Synchronization in Java is a mechanism that ensures only one thread can access a critical section of code at a time, preventing concurrent access issues and avoiding data inconsistency. This mechanism is essential when multiple threads access shared resources.
How Synchronization Works
When a thread enters a synchronized method or block, it acquires a lock on the object (or class, in the case of static synchronization), ensuring that only one thread can execute the synchronized code at a time while all other threads are in a waiting state.
- Synchronized methods: A method is synchronized by adding the
synchronized
keyword to its declaration. - Synchronized blocks: A block of code can be synchronized by wrapping it with a
synchronized
block.
Advantages and Disadvantages of Synchronization
Advantages:
- Ensures data consistency by preventing multiple threads from modifying shared resources simultaneously.
- Helps avoid issues like race conditions and deadlocks, ensuring thread safety.
Disadvantages:
- Increases waiting time for threads as other threads must wait for the lock to be released.
- Can lead to decreased performance if synchronization is not used judiciously.
Example: Synchronizing Methods
The synchronized
keyword can be applied to methods to ensure that only one thread can execute the method at a time.
class Counter {
private int count = 0;
// Synchronized method to ensure thread-safe increment
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Create two threads that will increment the counter
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
// Start the threads
t1.start();
t2.start();
// Wait for both threads to finish
t1.join();
t2.join();
// Print the final count after both threads have completed
System.out.println("Final count: " + counter.getCount());
}
}
In this example, the increment()
method is synchronized, ensuring that only one thread can increment the count at a time.
Example: Synchronizing Blocks
Instead of synchronizing an entire method, you can synchronize specific blocks of code within a method. This is often used for fine-grained control over which sections of code need synchronization.
class Counter {
private int count = 0;
public void increment() {
// Synchronizing only the critical section
synchronized(this) {
count++;
}
}
public int getCount() {
return count;
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Create two threads that will increment the counter
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
// Start the threads
t1.start();
t2.start();
// Wait for both threads to finish
t1.join();
t2.join();
// Print the final count after both threads have completed
System.out.println("Final count: " + counter.getCount());
}
}
This example demonstrates how to use a synchronized block to protect only the critical section of code where shared resources are accessed, providing more control over synchronization.
Static Synchronization
Static synchronization is used when a thread needs to lock the class itself (not just the instance). This is done by synchronizing static methods.
class Counter {
private static int count = 0;
// Synchronized static method
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
// Create two threads that will increment the counter
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
Counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
Counter.increment();
}
});
// Start the threads
t1.start();
t2.start();
// Wait for both threads to finish
t1.join();
t2.join();
// Print the final count after both threads have completed
System.out.println("Final count: " + Counter.getCount());
}
}
In this example, the static increment()
method is synchronized, which ensures that only one thread can access this method at a time across all instances of the class.
Inter-Thread Communication
Inter-thread communication allows threads to communicate using the wait()
, notify()
, and notifyAll()
methods. These methods allow threads to cooperate and coordinate their execution, especially when one thread needs to wait for a condition to be met by another thread.
The classic example of inter-thread communication is the producer-consumer problem, where one thread produces items and another consumes them. The consumer thread waits until the producer thread produces an item.
Producer-Consumer Example
class SharedResource {
private boolean available = false;
public synchronized void produce() throws InterruptedException {
while (available) {
wait(); // Wait until the item is consumed
}
System.out.println("Produced");
available = true; // Item is now produced
notify(); // Notify the consumer that the item is produced
}
public synchronized void consume() throws InterruptedException {
while (!available) {
wait(); // Wait until the item is produced
}
System.out.println("Consumed");
available = false; // Item is now consumed
notify(); // Notify the producer that the item is consumed
}
}
public class InterThreadExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
resource.produce();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
resource.consume();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
In this example, the producer thread produces an item and notifies the consumer thread, which consumes the item. The consumer thread waits if the item is not available. The producer and consumer threads synchronize their work using the wait()
and notify()
methods.
How It Works:
- The producer thread produces an item and sets the
available
flag totrue
. - If the consumer thread finds the
available
flag isfalse
, it callswait()
to wait until the item is available. - The producer, after producing the item, calls
notify()
to signal the consumer thread that the item is now available. - Similarly, the consumer thread, after consuming the item, calls
notify()
to signal the producer thread that the item is consumed.
This mechanism helps ensure that the producer and consumer threads work in sync, without one thread running ahead or being blocked unnecessarily.
Thread Safety and Deadlock
Thread safety refers to the ability of a program or a section of code to function correctly when multiple threads access it simultaneously. Deadlock occurs when two or more threads are blocked forever due to mutual resource locking.
Thread Safety Example:
Use synchronization to prevent thread interference when multiple threads access shared resources simultaneously.
class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class ThreadSafetyExample {
public static void main(String[] args) throws InterruptedException {
SafeCounter counter = new SafeCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Count: " + counter.getCount()); // Ensure thread safety
}
}
Deadlock Example:
Deadlock occurs when two or more threads are blocked forever due to circular resource dependency.
class Resource1 {
public synchronized void method1(Resource2 resource2) {
System.out.println("Thread 1: Holding Resource 1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
resource2.last();
}
public synchronized void last() {
System.out.println("Thread 1: In method last");
}
}
class Resource2 {
public synchronized void method2(Resource1 resource1) {
System.out.println("Thread 2: Holding Resource 2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
resource1.last();
}
public synchronized void last() {
System.out.println("Thread 2: In method last");
}
}
public class DeadlockExample {
public static void main(String[] args) {
final Resource1 resource1 = new Resource1();
final Resource2 resource2 = new Resource2();
Thread t1 = new Thread(() -> resource1.method1(resource2));
Thread t2 = new Thread(() -> resource2.method2(resource1));
t1.start();
t2.start();
}
}
In this example, two threads hold resources that the other thread needs, resulting in a deadlock.
Thread Pool Executor
The ExecutorService
interface provides methods to manage a pool of threads efficiently. The ThreadPoolExecutor
class manages a pool of worker threads to execute tasks.
Types of Executors in Java
In Java, executors are part of the java.util.concurrent
package, and they provide a high-level mechanism for managing and controlling threads. Executors manage a pool of worker threads and allow tasks to be executed asynchronously, improving the efficiency and responsiveness of multithreaded programs.
Executor Factory Methods
The Executor
class offers factory methods to create thread pools for managing and executing tasks. The following are the most commonly used factory methods:
newFixedThreadPool(int n)
: Creates a thread pool with a fixed number of threads. If all threads are busy, additional tasks are queued until a thread becomes available.newCachedThreadPool()
: Creates a thread pool that dynamically adjusts the number of threads based on demand. If a thread is idle for 60 seconds, it is removed from the pool.newSingleThreadExecutor()
: Creates a thread pool with a single worker thread that executes tasks sequentially. This is useful for tasks that must be executed in order.
How Executors Work
The basic workflow of how tasks are managed by executors is as follows:
- You submit a task to the executor using methods like
submit()
orexecute()
. - The executor places the task in a task queue.
- A worker thread from the pool retrieves the task from the queue and executes it.
- Once the task is complete, the worker thread returns to the pool, ready to execute another task.
Benefits of Using Executors
- Improved Responsiveness: Executors allow tasks to be executed concurrently, improving overall system responsiveness and reducing task wait time.
- Better Resource Utilization: Threads are reused, which reduces the overhead of creating and destroying threads, making the system more efficient.
- Enhanced Scalability: Executors make it easy to scale the system up or down based on workload demands. You can add or remove threads dynamically as needed.
Examples of Executors
1. Using newFixedThreadPool
The newFixedThreadPool()
method creates a fixed-size thread pool.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is executing a task.");
});
}
executor.shutdown();
}
}
In this example, we create a thread pool with 3 threads. The first 3 tasks are executed immediately, and the remaining 2 tasks are queued until a thread becomes available.
2. Using newCachedThreadPool
The newCachedThreadPool()
method creates a thread pool that dynamically adjusts the number of threads based on demand.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is executing a task.");
});
}
executor.shutdown();
}
}
In this example, threads are created as needed, and idle threads are removed after 60 seconds of inactivity.
3. Using newSingleThreadExecutor
The newSingleThreadExecutor()
method creates a thread pool with a single worker thread that executes tasks sequentially.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is executing a task.");
});
}
executor.shutdown();
}
}
In this example, even though multiple tasks are submitted, they are executed sequentially by the single worker thread in the pool.
Summary
Concept | Description |
---|---|
Multithreading | Allows concurrent execution of multiple threads within the same process, sharing the same memory space. |
Multiprocessing | Allows multiple processes to run simultaneously, each with its own memory space. |
Thread Safety | Ensures that shared resources are properly managed when accessed by multiple threads. |
Daemon Threads | Background threads that do not prevent the JVM from exiting. |
Synchronization | Prevents thread interference when multiple threads access shared resources. |
Inter-Thread Communication | Allows threads to communicate using wait() , notify() , and notifyAll() . |
Thread Pool Executor | Manages a pool of worker threads to efficiently handle multiple tasks concurrently. |