Tao
Tao

Ways to Create Threads in Java

This article explores the ways to create threads in Java based on JDK 11. My answer is that there are only two ways: extending the Thread class and implementing the Runnable interface, as stated in the Oracle official documentation.

The Thread class is provided by Java to represent a thread. It is a concrete class that implements the Runnable interface and offers additional thread control and management functionalities. By extending the Thread class, you can create custom thread classes.

The Runnable interface is a functional interface defined by Java, used to represent tasks that can be executed. By implementing the Runnable interface, you can encapsulate the task logic in the run() method and pass it to a Thread object to create a thread.

The Thread class itself implements the Runnable interface, while Runnable is just an interface. When you create and start a Thread, it opens a new thread and executes its run() method within this new thread. When you create a Runnable and pass it to the Thread constructor, the Thread will execute the run() method of the Runnable in the new thread.

Here is a code analysis showing the Thread class implementing the Runnable interface:

java

public class Thread implements Runnable {
    // ... other code omitted
    
    // Constructor accepting a Runnable instance
    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
        this(group, target, name, stackSize, null, true);
    }
    
    private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        // ... other code omitted
        // Assigning the Runnable instance to this.target field
        this.target = target;
        // ... other code omitted
    }
    
    @Override
    public void run() {
        // Key point: check if target is not null
        if (target != null) {
            target.run();
        }
    }
}

What happens if you both implement Runnable’s run method and override Thread’s run method?

Here’s the code:

java

public class ThreadDemo {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("Runnable run");
        }) {
            @Override
            public void run() {
                System.out.println("Thread run");
            }
        }.start();
    }
}

Output:

shell

Thread run

In Java, while both Runnable and Thread can be used for implementing multi-threading, using the Runnable interface is generally considered better for several reasons:

  • Avoid Inheritance Limitation: Java is a single-inheritance language. If you extend the Thread class to create a thread, you cannot extend any other class. Using the Runnable interface avoids this limitation as Java allows a class to implement multiple interfaces.

  • Improved Code Organization: By separating the task logic from the thread logic, using a class that implements the Runnable interface can better modularize and organize the code. This enhances code readability, maintainability, and testability.

  • Greater Flexibility: The Runnable interface allows the task code to be shared across multiple threads, providing more flexibility in switching tasks between threads. Thread class-based threads have tightly coupled task and thread code, which is less flexible.

  • Resource Sharing and Thread Pool Usage: Runnable interfaces support better use of thread pools. A thread pool is a mechanism for reusing threads efficiently and managing their lifecycle. By submitting Runnable tasks to a thread pool, you can better manage resources and improve system performance.

  • Extendability and Replaceability: If you need to change the thread implementation method in the future, such as using the Executor framework or other concurrency tools, using the Runnable interface makes migration and replacement easier. Using the Thread class might require significant code changes.

Overall, using the Runnable interface to implement Java threads is preferable because it provides better code organization, maintainability, and extensibility while avoiding inheritance limitations and supporting resource sharing and thread pool usage.

In Java, there are several ways to create threads. Here are some common methods for implementing threads:

  • Extending the Thread Class: One of the basic ways to implement a thread is to create a subclass of the Thread class, override the run() method to define the thread’s execution logic, create an instance of the subclass, and call the start() method to start the thread.

  • Implementing the Runnable Interface: Another common way to implement a thread is by implementing the Runnable interface. Define a class that implements the Runnable interface and its run() method. Then, create an instance of this class, pass it to the Thread constructor, and call the start() method of the Thread.

  • Using Callable and Future: Java provides the Callable and Future interfaces, allowing threads to execute and return a result. The Callable interface is similar to the Runnable interface but its call() method can return a value. You can use the submit() method of ExecutorService to submit a Callable task and get a Future object to retrieve the result.

  • Using Thread Pools: A thread pool is a mechanism for managing and reusing threads. By using the Executor framework provided by Java, you can create a thread pool and submit tasks to the thread pool for execution. This method improves thread efficiency and performance and manages the lifecycle of threads.

  • Using Java Concurrency Classes: Java provides several concurrency classes like Semaphore, CountDownLatch, CyclicBarrier, etc., to help with synchronization and coordination between multiple threads. These classes provide advanced thread control and synchronization mechanisms.

  • Using Timers: Java provides the Timer class and ScheduledExecutorService interface for scheduling tasks to be executed at specific times or periodically.

However, fundamentally there are only two methods: overriding the Thread#run method and implementing the Runnable#run method. At the core, both methods use the Thread class’s run method. Essentially, both methods are similar.

In essence, there are fundamentally two ways to create threads. Technically speaking, creating a thread involves constructing a Thread object, and the execution unit of the thread can be defined in two ways:

  1. Implementing the Runnable interface’s run method, which the Thread class’s run method will call.
  2. Overriding the Thread class’s run method by extending the Thread class.

Related Content