Multithreading & Concurrency

Running Multiple Tasks Simultaneously

← Back to Index

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

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?

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();
Why Runnable is Preferred
  • 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.

❌ Unsafe Code - Race Condition
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!)
✅ Safe Code - Synchronized
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

Threading 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:

When NOT to Use:

Why It Matters

Remember

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!"