Java Multithreading
Learn how to run multiple tasks simultaneously in Java - like juggling multiple balls at once, making your programs faster and more responsive!
What is Multithreading?
Multithreading is a feature that allows concurrent execution of two or more parts of a program for maximum utilization of CPU. Each part of such a program is called a thread. Think of it like a chef cooking multiple dishes at once - stirring one pot while another is boiling, making the entire cooking process faster!
A thread is a lightweight subprocess, the smallest unit of processing. Java provides built-in support for multithreaded programming, making it easier to create applications that can perform multiple tasks simultaneously.
- Concurrent Execution: Multiple threads run simultaneously, sharing CPU time
- Better Performance: Utilize multi-core processors efficiently
- Responsive Applications: Keep UI responsive while doing background tasks
- Resource Sharing: Threads share memory and resources of the process
- Independent Tasks: Each thread has its own execution path
๐ก Real-World Analogy: Imagine a restaurant kitchen where one chef prepares appetizers, another cooks main courses, and a third makes desserts - all at the same time. Each chef is like a thread, working independently but toward the same goal!
Ways to Create Threads in Java
1. Extending Thread Class
Create a class that extends the Thread class and override the run() method.
Advantage: Simple and straightforward approach
2. Implementing Runnable Interface
Create a class that implements Runnable interface and define the run() method.
Advantage: Better approach as your class can still extend another class
3. Using Lambda Expressions (Java 8+)
Use lambda expressions to create threads concisely.
Advantage: Clean and modern syntax for simple tasks
4. Using Executor Framework
Use ExecutorService for managing thread pools efficiently.
Advantage: Better resource management and control
Method 1: Extending Thread Class
Here's how to create a thread by extending the Thread class:
Example: Creating Thread by Extending Thread Class
// Thread class by extending Thread
class MyThread extends Thread {
private String taskName;
public MyThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(taskName + " started");
for (int i = 1; i <= 5; i++) {
System.out.println(taskName + ": Count " + i);
try {
Thread.sleep(500); // Sleep for 500ms
} catch (InterruptedException e) {
System.out.println(taskName + " interrupted");
}
}
System.out.println(taskName + " completed");
}
}
public class ThreadExample1 {
public static void main(String[] args) {
System.out.println("Main thread started");
// Creating thread objects
MyThread thread1 = new MyThread("Thread-1");
MyThread thread2 = new MyThread("Thread-2");
// Starting threads
thread1.start(); // Don't call run() directly!
thread2.start();
System.out.println("Main thread finished");
}
}
Main thread started Main thread finished Thread-1 started Thread-2 started Thread-1: Count 1 Thread-2: Count 1 Thread-1: Count 2 Thread-2: Count 2 Thread-1: Count 3 Thread-2: Count 3 Thread-1: Count 4 Thread-2: Count 4 Thread-1: Count 5 Thread-2: Count 5 Thread-1 completed Thread-2 completed
Explanation:
extends Thread- Create a class that inherits Thread classrun()- Contains the code that will be executed by the threadstart()- Starts the thread execution (never call run() directly!)Thread.sleep()- Pauses thread execution for specified milliseconds- Both threads run concurrently, their output may interleave
Method 2: Implementing Runnable Interface
This is the preferred approach as it allows your class to extend another class:
Example: Creating Thread with Runnable Interface
// Implementing Runnable interface
class MyTask implements Runnable {
private String taskName;
public MyTask(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(taskName + " is running");
for (int i = 1; i <= 3; i++) {
System.out.println(taskName + ": Step " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(taskName + " finished");
}
}
public class RunnableExample {
public static void main(String[] args) {
System.out.println("=== Runnable Interface Demo ===\n");
// Create Runnable objects
MyTask task1 = new MyTask("Download Task");
MyTask task2 = new MyTask("Upload Task");
// Create Thread objects and pass Runnable
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
// Start threads
thread1.start();
thread2.start();
System.out.println("Main thread continues...");
}
}
=== Runnable Interface Demo === Main thread continues... Download Task is running Upload Task is running Download Task: Step 1 Upload Task: Step 1 Download Task: Step 2 Upload Task: Step 2 Download Task: Step 3 Upload Task: Step 3 Download Task finished Upload Task finished
Explanation:
implements Runnable- Better approach, allows extending another classThread(Runnable target)- Pass Runnable object to Thread constructor- More flexible and follows composition over inheritance principle
- Recommended approach by Java best practices
Method 3: Using Lambda Expressions
Java 8+ allows you to create threads using lambda expressions for simple tasks:
Example: Threads with Lambda Expressions
public class LambdaThreadExample {
public static void main(String[] args) {
System.out.println("=== Lambda Thread Example ===\n");
// Method 1: Lambda with Runnable
Thread thread1 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Lambda Thread 1: " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// Method 2: Even more concise
Thread thread2 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Lambda Thread 2: " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
// Anonymous Runnable (older style)
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Anonymous thread running");
}
}).start();
}
}
=== Lambda Thread Example === Anonymous thread running Lambda Thread 1: 1 Lambda Thread 2: 1 Lambda Thread 1: 2 Lambda Thread 2: 2 Lambda Thread 1: 3 Lambda Thread 2: 3 Lambda Thread 1: 4 Lambda Thread 2: 4 Lambda Thread 1: 5 Lambda Thread 2: 5
Explanation:
- Lambda expressions provide clean, concise syntax for simple threads
() -> { }- Lambda syntax for Runnable (since it's a functional interface)- Perfect for quick, one-time thread tasks
- Makes code more readable and less verbose
Thread Lifecycle and States
Understanding the different states a thread goes through during its lifetime:
Thread States Visualization
State Description How to Reach โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ NEW Thread object created new Thread() RUNNABLE Ready to run or running start() called BLOCKED Waiting for monitor lock Waiting for synchronized block WAITING Waiting indefinitely wait(), join() TIMED_WAITING Waiting for specified time sleep(ms), wait(ms) TERMINATED Thread execution completed run() method finishes
๐ Thread Lifecycle: NEW โ RUNNABLE โ (RUNNING/BLOCKED/WAITING) โ TERMINATED. A thread can move between states but always starts at NEW and ends at TERMINATED.
Thread Synchronization
When multiple threads access shared resources, synchronization prevents data inconsistency:
Example: Synchronized Method
// Shared resource
class Counter {
private int count = 0;
// Synchronized method - only one thread can execute at a time
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() +
" incremented count to: " + count);
}
public int getCount() {
return count;
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Create multiple threads accessing the same counter
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter.increment();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter.increment();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread-2");
t1.start();
t2.start();
// Wait for both threads to complete
t1.join();
t2.join();
System.out.println("\nFinal count: " + counter.getCount());
}
}
Thread-1 incremented count to: 1 Thread-2 incremented count to: 2 Thread-1 incremented count to: 3 Thread-2 incremented count to: 4 Thread-1 incremented count to: 5 Thread-2 incremented count to: 6 Thread-1 incremented count to: 7 Thread-2 incremented count to: 8 Thread-1 incremented count to: 9 Thread-2 incremented count to: 10 Final count: 10
Explanation:
synchronizedkeyword ensures only one thread executes the method at a time- Prevents race conditions and data inconsistency
join()- Makes main thread wait for other threads to complete- Without synchronization, final count might be less than 10 due to race conditions
Common Thread Methods
Important methods for controlling thread behavior:
Example: Thread Control Methods
public class ThreadMethodsExample {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker thread started");
System.out.println("Thread name: " + Thread.currentThread().getName());
System.out.println("Thread priority: " + Thread.currentThread().getPriority());
for (int i = 1; i <= 3; i++) {
System.out.println("Working... " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Worker was interrupted!");
return;
}
}
System.out.println("Worker thread completed");
});
// Setting thread name and priority
worker.setName("Worker-1");
worker.setPriority(Thread.MAX_PRIORITY); // 10
System.out.println("Main thread name: " + Thread.currentThread().getName());
System.out.println("Is worker alive? " + worker.isAlive());
worker.start();
System.out.println("Is worker alive now? " + worker.isAlive());
// Main thread waits for worker to complete
worker.join(); // Wait for worker to finish
System.out.println("Is worker alive after join? " + worker.isAlive());
System.out.println("Main thread finished");
}
}
Main thread name: main Is worker alive? false Is worker alive now? true Worker thread started Thread name: Worker-1 Thread priority: 10 Working... 1 Working... 2 Working... 3 Worker thread completed Is worker alive after join? false Main thread finished
Explanation:
setName()/getName()- Set or get thread namesetPriority()- Set thread priority (1-10, default is 5)isAlive()- Check if thread is still runningjoin()- Wait for thread to complete before continuingcurrentThread()- Get reference to currently executing thread
Practical Example: Download Manager
Let's create a simple download manager that downloads multiple files concurrently:
Example: Multi-threaded Download Manager
class FileDownloader implements Runnable {
private String fileName;
private int fileSize; // in MB
public FileDownloader(String fileName, int fileSize) {
this.fileName = fileName;
this.fileSize = fileSize;
}
@Override
public void run() {
System.out.println("Started downloading: " + fileName);
try {
for (int i = 1; i <= fileSize; i++) {
// Simulate download time
Thread.sleep(500);
int progress = (i * 100) / fileSize;
System.out.println(fileName + ": " + progress + "% complete");
}
System.out.println("โ " + fileName + " downloaded successfully!");
} catch (InterruptedException e) {
System.out.println(fileName + " download interrupted!");
}
}
}
public class DownloadManager {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Download Manager ===\n");
// Create download tasks
FileDownloader file1 = new FileDownloader("video.mp4", 3);
FileDownloader file2 = new FileDownloader("music.mp3", 2);
FileDownloader file3 = new FileDownloader("document.pdf", 1);
// Create threads
Thread download1 = new Thread(file1);
Thread download2 = new Thread(file2);
Thread download3 = new Thread(file3);
// Start all downloads
long startTime = System.currentTimeMillis();
download1.start();
download2.start();
download3.start();
// Wait for all downloads to complete
download1.join();
download2.join();
download3.join();
long endTime = System.currentTimeMillis();
System.out.println("\nโ All downloads completed!");
System.out.println("Total time: " + (endTime - startTime) / 1000.0 + " seconds");
}
}
=== Download Manager === Started downloading: video.mp4 Started downloading: music.mp3 Started downloading: document.pdf document.pdf: 100% complete โ document.pdf downloaded successfully! video.mp4: 33% complete music.mp3: 50% complete video.mp4: 66% complete music.mp3: 100% complete โ music.mp3 downloaded successfully! video.mp4: 100% complete โ video.mp4 downloaded successfully! โ All downloads completed! Total time: 1.5 seconds
Explanation:
- Multiple files download simultaneously instead of one after another
- Each download runs in its own thread
- Main thread waits using join() for all downloads to complete
- Much faster than downloading files sequentially
Important Thread Concepts
Key Multithreading Terms
Concept Description โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Race Condition Multiple threads accessing shared data causing inconsistency Deadlock Two threads waiting for each other indefinitely Thread Safety Code that works correctly with multiple threads Synchronization Controlling access to shared resources Context Switching CPU switching between threads (has overhead) Thread Pool Reusable collection of threads (more efficient)
โ ๏ธ Common Pitfalls: Never call run() directly - always use start(). Calling run() executes code in the same thread, not creating a new one. Also, remember that synchronized methods have performance overhead, so use them wisely!
Best Practices for Multithreading
- Use Runnable over Thread: Prefer implementing Runnable for better flexibility
- Minimize Synchronization: Only synchronize when necessary to avoid performance issues
- Avoid Deadlocks: Always acquire locks in the same order
- Use Thread Pools: For better resource management with ExecutorService
- Handle InterruptedException: Properly respond to thread interruptions
- Make Threads Daemon When Appropriate: Use daemon threads for background tasks
๐ฏ When to Use Multithreading: Use threads for: parallel processing, responsive UIs, concurrent I/O operations, background tasks, server applications handling multiple clients, and any scenario where tasks can run independently.