Creating Threads

About

In Java, threads can be created and managed using multiple approaches. Each approach has its use cases and trade-offs. Some of the approaches are given below.

1. Extending the Thread Class

  • The simplest way to create a thread is by extending the Thread class.

  • The run() method is overridden to define the thread's behavior.

  • A new thread is started using the start() method.

Example:

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Thread is running...");
    }
}

public class SomeMain {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // Start the thread
        
        // Thread-0 Thread is running...
    }
}

Limitations:

  • Java does not support multiple inheritance, so extending Thread prevents extending other classes.

  • Better to use the Runnable interface if we need more flexibility.

2. Implementing the Runnable Interface

  • Instead of extending Thread, the Runnable interface can be implemented.

  • The run() method is implemented inside the class.

  • The thread is started using Thread class.

Example:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Thread is running...");
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
        // Thread-0 Thread is running...
    }
}

Advantages:

  • Allows the class to extend other classes (unlike extending Thread).

  • Encouraged in multi-threaded environments where tasks can be separated from threads.

3. Using Callable and Future (Return Value from Thread)

  • The Callable<T> interface (from java.util.concurrent) allows a thread to return a result.

  • The Future<T> interface is used to retrieve the result of the thread execution.

Example:

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {

    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName() + " Thread executed!";
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(new MyCallable());

        System.out.println(future.get()); // Retrieves the result
        executor.shutdown();
        
        // pool-1-thread-1 Thread executed!
    }
}

Advantages:

  • Unlike Runnable, Callable allows returning values and throwing exceptions.

4. Using Anonymous Class

  • Instead of defining a separate class, threads can be created using an anonymous class.

Example:

public class AnonymousExample {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " Thread is running...");
            }
        });
        thread.start();
        // Thread-0 Thread is running...
    }
}

Use Case:

  • Useful for quick thread creation without creating separate classes.

5. Using Lambda Expressions (Java 8+)

  • Java 8 introduced lambda expressions, making it even more concise to create threads.

public class AnonymousExample {
    // Using lambda expression
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println(Thread.currentThread().getName() + " Thread is running..."));
        
        System.out.println(Thread.currentThread().getName() + " Current Thread is running...");
        thread.start();
        
        /* Output
        main Current Thread is running...
        Thread-0 Thread is running...
        */
    }
}

Advantages:

  • Reduces boilerplate code for single-method interfaces like Runnable.

6. Using ThreadPoolExecutor (Efficient Thread Management)

  • Instead of creating a new thread every time, thread pools reuse existing threads.

  • ThreadPoolExecutor is a low-level API for managing thread pools.

Example:

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

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 1; i <= 5; i++) {
            executor.execute(() -> System.out.println(Thread.currentThread().getName() + " is running"));
        }
        executor.shutdown();
        
        /* Output
        pool-1-thread-1 is running
        pool-1-thread-2 is running
        pool-1-thread-3 is running
        pool-1-thread-2 is running
        pool-1-thread-1 is running
        */
    }
}

Advantages:

  • Reduces overhead of creating/destroying threads frequently.

  • Better resource management for high-load applications.

7. Using ForkJoinPool (Parallel Computing)

  • The Fork-Join framework is useful for parallel computing by dividing tasks into smaller subtasks.

Example:

package practice;

import java.util.concurrent.RecursiveTask;

public class MyRecursiveTask extends RecursiveTask<Integer> {

    private int num;

    MyRecursiveTask(int num) {
        this.num = num;
    }

    protected Integer compute() {
        if (num <= 1) {
            return num;
        }
        MyRecursiveTask task1 = new MyRecursiveTask(num - 1);
        MyRecursiveTask task2 = new MyRecursiveTask(num - 2);
        task1.fork();
        return task2.compute() + task1.join();
    }
}
import java.util.concurrent.ForkJoinPool;

public class ForkJoinExample {

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        System.out.println(pool.invoke(new MyRecursiveTask(5))); // 5
    }
}

Use Case:

  • Suitable for divide-and-conquer algorithms like parallel recursion and large dataset processing.

8. Using Virtual Threads (Java 19+)

  • Virtual Threads (introduced in Java 19) allow lightweight, high-performance thread execution.

  • Unlike OS threads, millions of virtual threads can be created without performance issues.

Example:

public class VirtualThreadExample {
    public static void main(String[] args) {
        Thread.startVirtualThread(() -> System.out.println("Virtual thread running..."));
    }
}

Advantages:

  • Extremely lightweight, highly scalable, and does not consume OS resources like traditional threads.

  • Ideal for handling highly concurrent workloads efficiently.

Comparison of Thread Creation Methods

Approach
Flexibility
Can Return Value?
Suitable For
Complexity

Extending Thread

Low

No

Simple tasks

Low

Implementing Runnable

Medium

No

Basic concurrency

Low

Using Callable & Future

Medium

Yes

Background tasks needing results

Medium

Using Anonymous Class

Medium

No

Quick thread execution

Low

Using Lambda Expressions

Medium

No

Shorter syntax

Low

Using ThreadPoolExecutor

High

No

Managing large tasks efficiently

Medium

Using ForkJoinPool

High

Yes

Parallel processing, recursion

High

Using Virtual Threads

High

No

Highly scalable concurrency

Low

Main Thread and New Thread Running Parallelly

After creating a new thread, the main thread continues to execute concurrently (parallelly) with the newly created thread.

Example

public class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("Child Thread: " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class MainThreadExample {

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // New thread starts

        // Main thread continues to execute in parallel
        for (int i = 1; i <= 5; i++) {
            System.out.println("Main Thread: " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        
        /* Output - The output order may vary due to thread scheduling
        Child Thread: 1
        Main Thread: 1
        Main Thread: 2
        Child Thread: 2
        Main Thread: 3
        Child Thread: 3
        Main Thread: 4
        Child Thread: 4
        Main Thread: 5
        Child Thread: 5 
        */
    }
}

How it Works?

  1. The main thread starts execution from main().

  2. It creates a new thread (MyThread) and starts it using start().

  3. The new thread (run()) runs independently while the main thread continues executing its own tasks.

  4. Both threads execute concurrently, and the CPU switches between them using the scheduler.

Concurrency vs. Parallelism

  1. Concurrency:

    • Multiple threads/tasks overlap in execution but may not execute at the exact same instant.

    • The CPU switches between threads rapidly, giving the illusion of simultaneous execution.

    • Occurs in single-core or multi-core systems.

  2. Parallelism:

    • Multiple threads/tasks execute at the exact same time on different CPU cores.

    • Requires a multi-core processor to truly run tasks simultaneously.

Why "Both threads execute concurrently" and not "parallelly"?

  • In Java, when we create a new thread, it does not guarantee that it will run on a separate core.

  • Java threads are managed by the JVM and the OS scheduler, which may time-share them on a single CPU.

  • If we have a single-core CPU, the threads can only run concurrently by switching back and forth.

  • If we have a multi-core CPU, the threads may run truly in parallel, but this depends on OS scheduling.

Example of Concurrency vs. Parallelism

1. Concurrent Execution (Single-Core CPU)

Time  | Thread-1 | Thread-2
--------------------------------
T1    | Running  | -
T2    | -        | Running
T3    | Running  | -
T4    | -        | Running

(Threads switch rapidly, but only one runs at a time.)

2. Parallel Execution (Multi-Core CPU)

Time  | Thread-1 | Thread-2
--------------------------------
T1    | Running  | Running
T2    | Running  | Running
T3    | Running  | Running

(Both threads run simultaneously on different CPU cores.)

Last updated

Was this helpful?