What is Multithreading?
Imagine you're cooking dinner: you put rice on the stove, while it's cooking you chop vegetables, and while those are simmering you set the table. You're doing multiple tasks at the same time by switching between them efficiently. That's exactly what multithreading does in programming!
Real-World Analogy: Restaurant Kitchen
- Single-threaded: One chef does everything - cook pasta, make salad, bake dessert - one after another. Customers wait forever!
- Multi-threaded: Multiple chefs work simultaneously - one on pasta, one on salad, one on dessert. Much faster!
What is a Thread?
A thread is like a worker that can execute code. Your Java program starts with one thread (the main thread), but you can create more threads to do multiple things at once.
// Without threads - tasks run one after another (sequential)
task1(); // Wait for task1 to finish
task2(); // Then do task2
task3(); // Then do task3
// Total time: time1 + time2 + time3
// With threads - tasks run simultaneously (concurrent)
Thread t1 = new Thread(() -> task1());
Thread t2 = new Thread(() -> task2());
Thread t3 = new Thread(() -> task3());
t1.start(); t2.start(); t3.start();
// Total time: max(time1, time2, time3) - much faster!
Why Use Multithreading?
- Performance: Utilize multiple CPU cores - faster execution
- Responsiveness: UI stays responsive while background work happens
- Resource Utilization: Don't waste CPU time waiting for I/O (file/network)
- Better User Experience: Download file while user continues working
Creating Threads - Two Ways
Method 1: Extending Thread Class
public class MyThread extends Thread {
@Override
public void run() {
// This code runs in a separate thread
for (int i = 1; i <= 5; i++) {
System.out.println("Thread: " + i);
try {
Thread.sleep(1000); // Sleep 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Usage
MyThread thread = new MyThread();
thread.start(); // Start the thread - calls run() automatically
Method 2: Implementing Runnable Interface (Preferred)
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Runnable: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Usage
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
Method 3: Using Lambda (Modern, Cleanest)
// Most concise way!
Thread thread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Lambda: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
- Java doesn't support multiple inheritance - extending Thread prevents extending other classes
- Implementing Runnable separates the task from the thread mechanism
- More flexible - can be used with ExecutorService (thread pools)
Complete Example - Multiple Threads Working Together
public class DownloadExample {
public static void main(String[] args) {
// Simulate downloading 3 files simultaneously
Thread download1 = new Thread(() -> downloadFile("File1.pdf"));
Thread download2 = new Thread(() -> downloadFile("File2.jpg"));
Thread download3 = new Thread(() -> downloadFile("File3.mp4"));
System.out.println("Starting downloads...");
// Start all downloads at once!
download1.start();
download2.start();
download3.start();
System.out.println("All downloads started! Main thread continues...");
}
private static void downloadFile(String fileName) {
System.out.println("Downloading " + fileName + "...");
try {
Thread.sleep(3000); // Simulate 3-second download
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(fileName + " downloaded!");
}
}
Output:
Starting downloads...
All downloads started! Main thread continues...
Downloading File1.pdf...
Downloading File2.jpg...
Downloading File3.mp4...
... 3 seconds pass ...
File1.pdf downloaded!
File2.jpg downloaded!
File3.mp4 downloaded!
Notice: All three files download simultaneously (concurrently), not one after another!
Thread Lifecycle - States
┌──────────┐
│ NEW │ ← Thread created but not started
└──────────┘
↓ start()
┌──────────┐
│ RUNNABLE │ ← Running or ready to run
└──────────┘
↓ sleep(), wait(), I/O
┌──────────┐
│ BLOCKED/ │ ← Waiting for resource/lock
│ WAITING │
└──────────┘
↓ notify(), timeout, I/O completes
┌──────────┐
│ RUNNABLE │ ← Back to running
└──────────┘
↓ run() completes
┌──────────┐
│TERMINATED│ ← Thread finished
└──────────┘
Important Thread Methods
start() - Begin Thread Execution
Thread t = new Thread(() -> System.out.println("Hello"));
t.start(); // Starts the thread - calls run() in new thread
// t.run(); // DON'T do this! Runs in current thread, not a new one
sleep() - Pause Thread
try {
Thread.sleep(2000); // Sleep for 2 seconds (2000 milliseconds)
} catch (InterruptedException e) {
e.printStackTrace();
}
join() - Wait for Thread to Finish
Thread worker = new Thread(() -> {
System.out.println("Working...");
try { Thread.sleep(3000); } catch (InterruptedException e) {}
System.out.println("Work done!");
});
worker.start();
System.out.println("Main thread continues...");
try {
worker.join(); // Wait for worker to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker finished, main thread continues");
isAlive() - Check if Thread is Running
Thread t = new Thread(() -> {
try { Thread.sleep(2000); } catch (InterruptedException e) {}
});
System.out.println("Before start: " + t.isAlive()); // false
t.start();
System.out.println("After start: " + t.isAlive()); // true
t.join();
System.out.println("After finish: " + t.isAlive()); // false
getName() / setName() - Thread Naming
Thread t = new Thread(() -> {
System.out.println("I am " + Thread.currentThread().getName());
});
t.setName("Worker-1");
t.start(); // Output: "I am Worker-1"
Common Problems with Multithreading
Problem 1: Race Condition
When multiple threads access shared data simultaneously and try to change it at the same time.
class Counter {
private int count = 0;
public void increment() {
count++; // NOT thread-safe!
}
public int getCount() {
return count;
}
}
// Two threads incrementing simultaneously
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(); t2.join();
System.out.println(counter.getCount()); // Expected: 2000, Actual: 1847 (varies!)
class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++; // Thread-safe! Only one thread at a time
}
public synchronized int getCount() {
return count;
}
}
// Now it works correctly!
System.out.println(counter.getCount()); // Always 2000
Problem 2: Deadlock
Two threads waiting for each other to release resources - both stuck forever!
// Thread A holds Lock1, wants Lock2
// Thread B holds Lock2, wants Lock1
// Both threads wait forever = DEADLOCK!
Analogy: Two people at a narrow bridge, both refuse to step back.
Practical Example - Web Server Handling Requests
public class SimpleWebServer {
public static void main(String[] args) {
// Simulate handling 5 client requests concurrently
for (int i = 1; i <= 5; i++) {
int clientId = i;
Thread clientHandler = new Thread(() -> {
handleRequest(clientId);
});
clientHandler.setName("Client-" + i);
clientHandler.start();
}
}
private static void handleRequest(int clientId) {
System.out.println(Thread.currentThread().getName() +
": Processing request from Client " + clientId);
try {
// Simulate processing time
Thread.sleep((long) (Math.random() * 3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
": Finished request for Client " + clientId);
}
}
Output:
Client-1: Processing request from Client 1
Client-2: Processing request from Client 2
Client-3: Processing request from Client 3
Client-4: Processing request from Client 4
Client-5: Processing request from Client 5
Client-3: Finished request for Client 3
Client-1: Finished request for Client 1
Client-5: Finished request for Client 5
Client-2: Finished request for Client 2
Client-4: Finished request for Client 4
Notice: All 5 clients are handled simultaneously! Without threads, each client would wait for the previous one to finish.
Best Practices
- ✅ Use thread pools (ExecutorService) instead of creating threads manually
- ✅ Implement Runnable rather than extending Thread
- ✅ Always handle InterruptedException properly
- ✅ Use synchronized or concurrent collections for shared data
- ✅ Avoid calling Thread.stop() - it's deprecated and dangerous
- ✅ Name your threads for easier debugging
- ✅ Keep synchronized blocks as small as possible
- ❌ Never call run() directly - always use start()
- ❌ Avoid creating too many threads - use thread pools
- ❌ Don't share mutable state without synchronization
When to Use Multithreading?
Good Use Cases:
- I/O Operations: Reading files, network calls, database queries
- UI Applications: Keep UI responsive while background work happens
- Parallel Processing: Process large datasets by splitting work
- Server Applications: Handle multiple client requests simultaneously
- Batch Processing: Process multiple items concurrently
When NOT to Use:
- Simple, quick operations (overhead not worth it)
- When operations must happen in strict order
- When shared state is complex (synchronization overhead)
- CPU-bound tasks on single-core machines
Why It Matters
- Modern Computing: Most devices have multiple cores - multithreading utilizes them
- User Experience: Apps stay responsive - no freezing
- Performance: Can significantly speed up applications
- Scalability: Handle more users/requests simultaneously
- Professional Skill: Essential for building real-world applications
Multithreading is powerful but complex. Start simple, understand the basics, then move to advanced topics like thread pools, locks, and concurrent collections.
"With great power comes great responsibility - and great complexity!"