Multithreading is a technique of programming in which a program can perform several tasks at the same time by breaking these tasks into smaller threads that may run independently. But it also helps to improve the performance of applications, making them more responsive and efficient.
Because Java is a solid and frequently used programming language, it offers considerable support for multithreading via its rich sets of APIs. Multithreading is important for modern software development whether you are trying to improve responsiveness of user interfaces, run background tasks, or trying to optimize CPU utilization.
In this blog we will go over basic multithreading from the ground up, building towards some more advanced topics. You’ll also learn how to use this feature effectively in Java with clear explanations and with practical examples. Let’s dive in!
What is a Thread?
A thread is the smallest unit of a program that can run independently. In Java:
- Each thread has its own lifecycle.
- Threads within the same application share memory, making communication efficient but requiring synchronization.
Key Concepts in Multithreading
Process vs Thread
- Process: An independent program running in memory.
- Thread: A smaller, lightweight unit within a process.
Difference Between Multithreading and Multitasking
Aspect | Multithreading | Multitasking |
Scope | Multiple threads in one process | Multiple processes running simultaneously |
Memory | Threads share memory | Processes have isolated memory |
Speed | Faster (less overhead) | Slower due to process isolation |
Key Features of Multithreading in Java
- Concurrency: Concurrent execution with multiple threads reduces idle CPU time.
- Resource Sharing: The reason for threads being able to share the same memory space is that communication is efficient.
- Independent Execution: Each thread runs independently, so failure in one thread doesn’t affect others.
- Synchronization Support: Java provides tools to manage thread interference and memory consistency.
Creating Threads in Java
1. Extending the Thread Class
In Java, you can create threads by extending the Thread class.
Code
// Define a thread by extending Thread
class MyThread extends Thread {
public void run() {
// Task to be performed by the thread
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread(); // Create a thread
thread.start(); // Start the thread
}
}
Explanation:
- run() Method: Contains the task the thread will execute.
- start() Method: Begins the thread execution by calling run() internally.
2. Implementing the Runnable Interface
If you want your class to extend another class (Java doesn’t support multiple inheritance), you can use the Runnable interface.
class MyRunnable implements Runnable {
public void run() {
// Task for the thread
System.out.println("Runnable thread is running: " + Thread.currentThread().getName());
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable()); // Pass Runnable to Thread
thread.start(); // Start the thread
}
}
Why use Runnable?
- It’s more flexible and promotes code reusability.
Thread Lifecycle
Threads in Java go through the following states:
- New: Thread is created but not started.
- Runnable: Thread is ready to run but waiting for the CPU.
- Running: Thread is executing.
- Waiting/Blocked: Thread is waiting for resources or another thread.
- Terminated: Thread has completed execution.
Thread Synchronization
When multiple threads access shared resources, synchronization ensures data consistency by allowing only one thread to access the resource at a time.
Example: Synchronizing a Counter
Code
class Counter {
private int count = 0;
public synchronized void increment() {
count++; // Increment the count
}
public int getCount() {
return count;
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new 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();
});
t1.start();
t2.start();
t1.join(); // Wait for t1 to finish
t2.join(); // Wait for t2 to finish
System.out.println("Final count: " + counter.getCount());
}
}
Explanation:
- synchronized Keyword: Ensures that only one thread can execute increment() at a time.
- join() Method: Waits for threads to complete before proceeding.
Exception Handling in Threads
Threads may encounter exceptions, such as invalid input or divide-by-zero errors. Proper exception handling ensures stable execution.
1. Using try-catch
Code
class ExceptionThread extends Thread {
public void run() {
try {
int result = 10 / 0; // Will throw ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Exception handled: " + e.getMessage());
}
}
}
public class ExceptionHandlingExample {
public static void main(String[] args) {
ExceptionThread thread = new ExceptionThread();
thread.start();
}
}
2. Using UncaughtExceptionHandler
Code
class RiskyThread extends Thread {
public void run() {
int result = 10 / 0; // Throws exception
}
}
public class UncaughtHandlerExample {
public static void main(String[] args) {
Thread thread = new RiskyThread();
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("Exception in thread " + t.getName() + ": " + e.getMessage());
});
thread.start();
}
}
Key Points:
- Try-catch is for handling expected exceptions locally.
- UncaughtExceptionHandler catches global exceptions in threads.
Thread Pooling
Thread pools reuse a fixed number of threads to execute multiple tasks, reducing the overhead of thread creation.
Example Using Executor Service
Code
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3); // Pool size of 3
for (int i = 1; i <= 5; i++) {
final int task = i;
executor.execute(() -> {
System.out.println("Executing task " + task + " on thread: " + Thread.currentThread().getName());
});
}
executor.shutdown(); // Shutdown the pool
}
}
Explanation:
- FixedThreadPool: Limits the number of threads running simultaneously.
- Efficient for handling large numbers of short-lived tasks.
Advanced Multithreading Concepts
- Thread Priority: Threads can be assigned priorities (MIN_PRIORITY, NORM_PRIORITY, MAX_PRIORITY).
- Thread Group: Threads can be grouped and managed collectively.
- Volatile Keyword: Ensures visibility of changes to a variable across threads.
Multithreading in Java Example
Here’s an example of multithreading where threads perform different tasks concurrently.
Code
// Multithreading Example
class PrintNumbers implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Number: " + i + " by Thread: " + Thread.currentThread().getId());
}
}
}
class PrintAlphabets implements Runnable {
@Override
public void run() {
for (char c = 'A'; c <= 'E'; c++) {
System.out.println("Alphabet: " + c + " by Thread: " + Thread.currentThread().getId());
}
}
}
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(new PrintNumbers());
Thread t2 = new Thread(new PrintAlphabets());
t1.start(); // Start number printing thread
t2.start(); // Start alphabet printing thread
}
}
Output:
Number: 1 by Thread: 13
Alphabet: A by Thread: 14
Number: 2 by Thread: 13
Alphabet: B by Thread: 14
Conclusion
Multithreading in Java allows developers to write efficient, responsive, and scalable programs. From thread creation using Thread or Runnable to synchronization, exception handling, and thread pooling, Java provides robust APIs to manage concurrency.
By mastering these concepts and applying them effectively, you can build powerful, multi-threaded applications.
FAQs about Multithreading in Java
The JVM uses the underlying operating system’s threading model to manage threads. In most modern JVMs, threads are mapped to native OS threads, enabling efficient scheduling and execution. The JVM also manages thread priorities and provides a memory model to ensure safe interaction between threads.
Yes, multiple threads can access the same method unless the method is marked as synchronized. Without synchronization, threads may cause race conditions or inconsistent data. Synchronizing methods or blocks ensures only one thread can access the critical section at a time.
Thread starvation occurs when a thread is unable to gain CPU time due to higher-priority threads monopolizing the processor. It can be avoided by:
-Using fair locks (ReentrantLock with fairness policy).
-Avoiding excessive thread priority adjustments.
-Ensuring a balanced thread pool configuration in the application.
– Wait(): Releases the monitor lock and suspends the thread until another thread calls notify() or notifyAll(). It is used for inter-thread communication.
– Sleep(): Pauses the thread for a specified time without releasing the lock. It is used for timing delays.
A deadlock occurs when two or more threads are waiting indefinitely for each other to release locks, causing the program to freeze.
Prevention techniques:
-Avoid nested locks.
-Use a consistent lock acquisition order.
-Use tryLock() from ReentrantLock to avoid indefinite blocking.