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");
    }
}
Output:
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 class
  • run() - Contains the code that will be executed by the thread
  • start() - 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...");
    }
}
Output:
=== 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 class
  • Thread(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();
    }
}
Output:
=== 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());
    }
}
Output:
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:

  • synchronized keyword 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");
    }
}
Output:
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 name
  • setPriority() - Set thread priority (1-10, default is 5)
  • isAlive() - Check if thread is still running
  • join() - Wait for thread to complete before continuing
  • currentThread() - 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");
    }
}
Output:
=== 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.