Understanding the Single Fundamental Method for Creating Threads in Java

In this section, we will explore why it is fundamentally accurate to say there is only one way to create threads. We will also examine the advantages of implementing the Runnable interface over extending the Thread class.

Creating threads is a fundamental aspect of concurrent programming, as we must first implement multithreading before proceeding with any further operations. This section starts by explaining how to create threads and aims to help you build a solid foundation. Although creating threads may seem simple and basic, it actually contains hidden complexities. Let's start by understanding why there is essentially only one way to create threads.

How many ways are there to create threads? Many people might say there are 2, 3, or 4 ways, but few would say there is only 1. Let's look at what these refer to. The description of the two most basic methods is widely known, so let's begin by examining their source code.

1. Implementing the Runnable Interface

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Using the Runnable interface to create a thread");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myTask = new MyRunnable();
        Thread thread = new Thread(myTask);
        thread.start();
    }
}

The first method involves implementing the Runnable interface, as shown in the code. First, the MyRunnable class implements the Runnable interface, then overrides the run() method. By passing the instance that has implemented the run() method into the Thread class, we can create a thread.

2. Extending the Thread Class

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Using the Thread class to create a thread");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

The second method involves extending the Thread class, as shown in the code. Unlike the first method, this one does not implement an interface but instead extends the Thread class and overrides its run() method. You are likely familiar with these two methods and use them frequently in your work.

3. Creating Threads Using a Thread Pool

Why do some people consider there to be a third or fourth method? Let's look at the third method: creating threads using a thread pool. Let's analyze the source code of the thread pool to see how it creates threads.

static class DefaultThreadFactory implements ThreadFactory {

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : 
            Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r, namePrefix + 
            threadNumber.getAndIncrement(), 0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

For a thread pool, it essentially creates threads through a thread factory. It uses the default DefaultThreadFactory, which sets some default values for the threads created by the thread pool, such as the thread name, weather it is a daemon thread, and the thread priority. However, no matter how these attributes are set, it still ultimately uses new Thread() to create threads, although the constructor receives more parameters. From this, we can see that creating threads using a thread pool does not go beyond the initial two basic methods because it is still based on new Thread().

Summary: In interviews, if you know this method but don't understand its underlying implementation, you may struggle during the interview. To better demonstrate yourself, you should avoid falling into traps by not fully understanding the details.

4. Creating Threads Using Callable

When answering questions about creating threads, after describing the first two methods, you can further mention that you also know about thread pools and Callable being able to create threads, but they are essentially based on the initial two methods. This answer can become a bonus in interviews. Then, the interviewer may ask about the components and principles of thread pools, which will be discussed in later lessons.

Creating threads with a return value using Callable:

class MyCallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }
}
// Create a thread pool
ExecutorService service = Executors.newFixedThreadPool(10);
// Submit the task and get the result via Future
Future<Integer> future = service.submit(new MyCallableTask());

As shown in the code, the Callable interface is implemented, and its generic type is set to Integer, returning a random number. Creating threads with Runnable is without a return value, while Callable and related Future and FutureTask can return the results of thread execution. However, regardleess of whether it is Callable or FutureTask, they are tasks that need to be executed, not threads themselves. They can be placed in a thread pool for execution, as shown in the code, where the submit() method places the task in the thread pool, and the thread pool creates the thread. No matter what method is used, the final execution is done by the thread, and the creation of the child thread still does not go beyond the initial two basic methods: implementing the Runnable interface or extending the Thread class.

5. Other Methods

  1. Timer
class TimerThread extends Thread {
// Specific implementation
}

At this point, you might say you know other ways to create threads, such as timers. If you create a Timer that runs a task every 10 seconds or after two hours, it indeed creates a thread and executes the task. However, if we analyze the Timer source code, we find that it ultimately has a TimerThread that inherits from Thread. Therefore, the timer method of creating threads eventually returns to the initial two methods.

  1. Other Methods
/**
 * Description: Creating a thread using an anonymous inner class
 */
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}).start();

// Anonymous inner class
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();

You might also say you know other methods, such as using anonymous inner classes or lambda expressions. In reality, these methods only implement threads at the syntactic level and cannot be considered as new ways to create threads. As shown in the code for creating threads using an anonymous inner class, it simply uses an anonymous inner class to instantiate the Runnable passed in.

6. There is Only One Way to Create Threads

Regarding this issue, we will not focus on why there is only one way to create threads. Instead, we assume there are two ways to create threads, and other methods, such as thread pools or timers, are just wrappers around new Thread(). If we consider these as new methods, the number of ways to create threads could become endless. For example, with JDK updates, new classes may be added, re-wrapping new Thread(), which appears as a new way to create threads. Looking beyond the surface, we find that after removing the wrapper, they are all based on the Runnable interface or extending the Thread class.

Next, we will delve deeper into why these two methods are essentially the same.

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

6.1 Analyzing the Two Implementation Methods

First, starting a thread requires calling the start() method, which eventually calls the run() method. Let's look at how the run() method is implemented in the first method. We can see that the run() method's code is very short. The first line of code checks if target is not null; if it is not null, it executes the second line of code, target.run(). The target is actually a Runnable, which is the object passed to the Thread class when implementing the Runnable interface.

Then, we look at the second method, which is extending the Thread class. After extending the Thread class, the run() method is overridden. After overriding, the run() method directly contains the task to be executed. However, it still needs to call the thread.start() method to start the thread, and the start() method eventually calls the overridden run() method to execute the task. At this point, we can clearly understand that creating a thread is essentially only one way: constructing a Thread class, which is the only way to create a thread.

6.2 Where Does the Execution Content Come From?

We have already understood that the two methods of creating threads are essentially the same. Their differences lie only in how the execution content is implemented. So where does the execution content come from?

Execution content mainly comes from two sources: either from the target or from the overridden run() method. Based on this, we can describe it as follows: essentially, there is only one way to create threads, and to implement the content that the thread executes, there are two ways: either by implementing the Runnable interface or by extending the Thread class and overriding the run() method. Based on this, if we want more ways to create threads, such as thread pools and Timer, we can wrap it accordingly.

6.3 Why Is Implementing the Runnable Interface Better Than Extending the Thread Class?

Now, we will compare the two methods of implementing thread content, i.e., why is implementing the Runnable interface better than extending the Thread class to create threads? What are the benefits?

First, considering the code architecture, the Runnable interface only has a single run() method, defining the content to be executed. In this case, implementing Runnable decouples the Thread class from the actual execution logic. The Thread class handles thread startup and attribute settings, with clear responsibilities.

Second, in certain cases, performance can be improved. When using the Thread class extension method, each time a task is executed, a new independent thread is created. After the task is completed, the thread reaches the end of its life cycle and is destroyed. If the task needs to be executed again, a new class that extends the Thread class must be created. If the content to be executed is minimal, such as simply printing a line in the run() method, the overhead is not significant. Compared to the entire process of creating and destroying a thread, this overhead is much larger, leading to a loss greater than the gain. If we use the Runnable interface method, we can pass the task directly into the thread pool, using fixed threads to complete the task without creating and destroying threads each time, significantly reducing performance overhead.

Third, a benefit is that Java does not support multiple inheritance. If our class extends the Thread class, it cannot inherit any other class afterward. This limits the flexibility of the code in the future, making it impossible to add functionality by inheriting other classes.

In summary, we should prioritize creating threads by implementing the Runnable interface.

This section has covered the various methods of creating threads, including implementing the Runnable interface and extending the Thread class. We have also analyzed why there is essentially only one way to create threads and why implementing the Runnable interface is better than extending the Thread class. After completing this section, you should have a deeper understanding of creating threads.

7. Summary:

  • There is only one way to create threads, which is to construct a Thread class.
  • The reason is that Thread implements the Runnable interface. A standalone class that implements Runnable is meaningless because it requires a current thread to support it. Thread is the foundation that supports the actual execution of Runnable. Runnable defines the specifications for the content executed by the thread as a functional interface. Strictly speaking, Runnable is not a way to implement threads but a way to define the content executed by custom threads (which can be the current working thread or a custom thread that extends Thread).
  • There are two ways to implement the content executed by threads: 1. Implementing the Runnable interface, 2. Extending the Thread class and overriding the run() method.
  • Benefits of implementing the Runnable interface: 1. Decoupling, 2. Improved performance, 3. Java does not support multiple inheritance, and using interfaces allows for multiple inheritance.

Tags: java multithreading Concurrency Runnable thread

Posted on Fri, 08 May 2026 12:05:20 +0000 by wdallman