Updated: July 23, 2025

In modern software development, efficient use of system resources and the ability to perform multiple tasks simultaneously are critical for creating responsive and high-performance applications. Multithreading in Java is a powerful technique that allows multiple threads to run concurrently within a single program, enabling parallel execution and improved utilization of CPU time.

This article explores the fundamental concepts of multithreading in Java, focusing on how threads are created, managed, and executed. We will delve into the basics of Java threads, different methods for creating threads, thread lifecycle, and practical examples to help you master multithreading basics.

Understanding Multithreading

What is a Thread?

A thread is a lightweight subprocess , the smallest unit of processing that can be scheduled by an operating system. In Java, a thread is an independent path of execution within a program. By default, every Java program has at least one thread , the main thread that begins executing from the main() method.

Why Use Multithreading?

  • Improved Performance: Multithreading allows multiple operations to be executed in parallel, leveraging multi-core processors for better performance.
  • Resource Sharing: Threads share the same memory space and resources of their parent process, making communication between them faster and easier.
  • Responsiveness: In GUI applications or server-side applications, multithreading enables programs to remain responsive by delegating long-running operations to background threads.
  • Better Resource Management: Threads can help manage asynchronous tasks such as file I/O, network communication, or time-consuming computations without blocking the main program flow.

Java Thread Model

Java provides built-in support for multithreaded programming through the java.lang.Thread class and the java.lang.Runnable interface.

Each thread in Java has:

  • A name for identification.
  • A priority that helps the thread scheduler decide when each thread should run.
  • A state such as New, Runnable, Running, Blocked, Waiting, Timed Waiting, or Terminated.
  • A program counter indicating the current execution point.
  • Its own stack memory for method invocation.

Creating Threads in Java

There are two primary ways to create a thread in Java:

  1. Extending the Thread class
  2. Implementing the Runnable interface

Both approaches have their advantages and disadvantages. Let’s explore each method with detailed examples.

1. Extending the Thread Class

The simplest way to create a thread is to define a new class that extends java.lang.Thread. You override its run() method to specify the code that should execute concurrently.

Example: Extending Thread Class

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running: " + Thread.currentThread().getName());
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + " prints " + i);
            try {
                Thread.sleep(500); // Pause for half a second
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        }
    }
}

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

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

        System.out.println("Main method ends");
    }
}

Explanation:

  • The MyThread class extends Thread and overrides the run() method.
  • Calling start() on a Thread object causes the JVM to invoke run() on a new call stack.
  • The main method creates two threads (t1 and t2) and starts them.
  • Both threads execute concurrently along with the main thread.

Notes:

  • You must call start() to begin new thread execution; calling run() directly will not create a new thread but will execute on the current thread instead.
  • Overriding run() without calling start() means no new thread is created.

2. Implementing Runnable Interface

Implementing the Runnable interface involves defining a class that implements its single method: run(). An instance of this class is then passed to a Thread object which manages execution.

This approach is preferred when your class already extends another class because Java only supports single inheritance (extends one class only).

Example: Implementing Runnable Interface

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable running: " + Thread.currentThread().getName());
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + " prints " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println("Runnable interrupted");
            }
        }
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        Runnable runnableTask = new MyRunnable();
        Thread t1 = new Thread(runnableTask);
        Thread t2 = new Thread(runnableTask);

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

        System.out.println("Main method ends");
    }
}

Explanation:

  • The class implements Runnable and overrides run().
  • A single instance of MyRunnable is passed into two separate threads (t1, t2).
  • When started, both threads run concurrently.

Advantages:

  • Since you implement an interface rather than extend a class, it provides more flexibility in class design.
  • Allows sharing data between threads if they share the same runnable instance (requires synchronization to avoid race conditions).

Thread Lifecycle in Java

Understanding a thread’s lifecycle helps in managing its behavior during execution. Here are key states:

State Description
New Thread object is created but not yet started
Runnable After invoking start(), ready to run or running
Running The thread scheduler picks it for execution
Blocked Waiting to acquire a lock or resource
Waiting Waiting indefinitely until notified by another thread
Timed Waiting Waiting for specified time duration
Terminated After completing execution or due to an unhandled exception

Transitions between states happen via methods like .start(), .sleep(), .wait(), .notify(), etc.

Important Methods in Thread Class

Here are some useful methods available in the Thread class for managing threads:

  • .start(): Starts thread execution; calls internal JVM code that invokes run().
  • .run(): Contains code executed by the thread; normally not called directly.
  • .sleep(long millis): Causes current thread to pause execution for given milliseconds.
  • .join(): Waits for this thread to finish before continuing execution of current thread.
  • .interrupt(): Interrupts a sleeping or waiting thread.
  • .setPriority(int priority): Sets priority between 1 (MIN_PRIORITY) and 10 (MAX_PRIORITY).
  • .getName() / .setName(String name): Gets or sets the name of the thread.

Synchronization Basics in Multithreading

Since multiple threads share memory space, concurrent access to shared variables can lead to inconsistent results called race conditions. To avoid this, Java provides synchronization techniques.

The keyword synchronized ensures only one thread accesses critical section at a time.

Example using synchronized block:

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SyncExample implements Runnable {
    private Counter counter;

    SyncExample(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for(int i=0; i<1000; i++) {
            counter.increment();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(new SyncExample(counter));
        Thread t2 = new Thread(new SyncExample(counter));

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

        t1.join();
        t2.join();

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

Without synchronization, final count may be less than expected because increments overlap.

Best Practices and Tips

  1. Use Runnable over extending Thread: It’s more flexible and promotes separation of concerns.
  2. Avoid using deprecated methods: For example, stop(), suspend(), resume() are unsafe.
  3. Handle InterruptedException properly: Always check for interruption especially when using sleep or wait.
  4. Keep critical sections short: To avoid performance bottlenecks when using synchronization.
  5. Use higher-level concurrency utilities: Classes like ExecutorService, Callable, and constructs from java.util.concurrent package provide robust threading capabilities beyond low-level Threads.
  6. Be cautious with shared mutable data: Use synchronization or atomic classes like AtomicInteger where necessary.

Conclusion

Java’s multithreading capabilities provide powerful tools for concurrent programming. Creating threads using either extending the Thread class or implementing the Runnable interface lets you perform multiple tasks simultaneously within your application. Understanding how threads behave , their lifecycle, states, methods available , is key to writing efficient multithreaded programs.

While multithreading can greatly enhance application responsiveness and throughput, it also introduces complexity such as race conditions and deadlocks if not carefully managed. Mastery of basic concepts like thread creation, synchronization, lifecycle management paves the way toward leveraging more advanced concurrency utilities effectively.

As you build confidence working with threads in Java, consider exploring additional frameworks like Fork/Join framework or reactive programming paradigms that simplify concurrent task management with improved scalability and safety.

Multithreading remains essential knowledge in today’s multi-core processing environment , with these basics under your belt, you’re well-positioned to develop high-performing Java applications tailored for concurrency challenges ahead.