Java Multithreading

Multithreading allows a program to execute multiple tasks simultaneously within the same process. Each task runs in its own thread — a lightweight unit of execution. Java has built-in support for multithreading through the java.lang.Thread class and the Runnable interface.

Key Concepts

  • Process: A running program with its own memory space.
  • Thread: A single path of execution within a process. A process can have multiple threads sharing the same memory.
  • Multithreading: Running multiple threads simultaneously to improve performance and responsiveness.

Real-World Analogy

Think of a restaurant kitchen. One chef handles desserts, another handles main courses, and a third handles salads — all working simultaneously. Multithreading is the same idea: multiple threads performing different tasks at the same time within one program.

Thread Life Cycle

A thread goes through several states during its lifetime:

  1. New: Thread object created, but not started yet.
  2. Runnable: start() called; thread is ready to run.
  3. Running: Thread is executing.
  4. Blocked/Waiting: Thread is paused (waiting for a resource or another thread).
  5. Terminated: Thread has completed execution.

Creating Threads – Two Ways

Method 1 – Extending the Thread Class

Create a class that extends Thread and override the run() method with the task to be performed.

class PrintNumbers extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(getName() + " → " + i);
            try {
                Thread.sleep(100);   // pause for 100ms
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted.");
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        PrintNumbers t1 = new PrintNumbers();
        PrintNumbers t2 = new PrintNumbers();

        t1.setName("Thread-A");
        t2.setName("Thread-B");

        t1.start();   // starts the thread
        t2.start();
    }
}

Sample Output (order may vary each run):

Thread-A → 1
Thread-B → 1
Thread-A → 2
Thread-B → 2
...

Method 2 – Implementing the Runnable Interface (Preferred)

Implementing Runnable is preferred because Java supports only single inheritance. Using Runnable keeps the option to extend another class.

class DownloadTask implements Runnable {
    private String fileName;

    DownloadTask(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println("Downloading " + fileName + " – " + (i * 33) + "% complete");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        System.out.println(fileName + " download complete.");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(new DownloadTask("report.pdf"));
        Thread t2 = new Thread(new DownloadTask("photo.jpg"));

        t1.start();
        t2.start();
    }
}

Thread Sleep

Thread.sleep(milliseconds) pauses the current thread for the specified amount of time. It throws InterruptedException, which must be handled.

try {
    Thread.sleep(1000);   // sleep for 1 second
} catch (InterruptedException e) {
    e.printStackTrace();
}

Thread Priority

Threads can be assigned a priority (1 to 10) that suggests to the JVM which thread should run first. The JVM scheduler is not guaranteed to follow priority strictly.

Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);

t1.setPriority(Thread.MAX_PRIORITY);   // 10
t2.setPriority(Thread.MIN_PRIORITY);   // 1

t1.start();
t2.start();

Synchronization

When multiple threads access shared data, problems can occur — one thread may read a value while another is modifying it, causing inconsistent results. This is called a race condition.

The synchronized keyword ensures that only one thread can access a method or block at a time.

Without Synchronization (Problem)

class Counter {
    int count = 0;

    void increment() {
        count++;   // not thread-safe
    }
}

With Synchronization (Solution)

class Counter {
    int count = 0;

    synchronized void increment() {
        count++;   // only one thread can execute this at a time
    }
}

public class Main {
    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();
        t2.join();

        System.out.println("Final count: " + counter.count);   // 2000
    }
}

The join() Method

join() makes the calling thread wait until the specified thread finishes its execution before continuing. Useful to ensure a thread completes before reading its results.

t1.start();
t1.join();   // main thread waits here until t1 finishes
System.out.println("t1 has finished.");

Lambda Threads (Java 8+)

With lambda expressions, creating a thread becomes more concise:

Thread t = new Thread(() -> {
    System.out.println("Running in a lambda thread.");
});
t.start();

Executor Framework (Modern Approach)

The ExecutorService from java.util.concurrent manages a pool of threads — a more scalable and controlled way to handle concurrent tasks.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
            });
        }

        executor.shutdown();
    }
}

Summary

  • Multithreading allows multiple tasks to run concurrently within one program.
  • Threads are created by extending Thread or implementing Runnable.
  • Implementing Runnable is preferred as it supports better design flexibility.
  • Thread.sleep() pauses a thread; join() waits for a thread to finish.
  • The synchronized keyword prevents race conditions on shared data.
  • The ExecutorService provides a managed, scalable way to run thread pools.

Leave a Comment

Your email address will not be published. Required fields are marked *