01 Why Is There Only One Way to Implement Threads

01 Why Is There Only One Way to Implement Threads #

In this lesson, we will mainly learn why it is said that there is essentially only one way to implement threads. What are the advantages of implementing threads through the Runnable interface compared to inheriting the Thread class?

Implementing threads is the foundation of concurrent programming, because we must first implement multithreading before we can continue with a series of operations. So let’s start with the basics of how to implement threads in concurrent programming. I hope you can solidify your foundation. Although implementing threads may seem simple and basic, it actually contains hidden mysteries. First, let’s look at why it is said that there is essentially only one way to implement threads.

How many ways are there to implement threads? Most people would say there are 2, 3, or 4 ways, and very few would say there is only 1 way. Let’s take a look at what they specifically mean. The description of the 2 implementation methods is the most basic and best known. Let’s start by looking at the source code of the 2 thread implementation methods.

Implementing the Runnable interface #

public class RunnableThread implements Runnable {

    @Override
    public void run() {
        System.out.println('Implementing threads by implementing the Runnable interface');
    }
}

The first method is to implement multithreading by implementing the Runnable interface, as shown in the code. First, the RunnableThread class implements the Runnable interface, and then the run() method is overridden. After that, you only need to pass an instance of the Runnable implementation with the run() method to the Thread class to implement multithreading.

Inheriting the Thread class #

public class ExtendsThread extends Thread {

    @Override
    public void run() {
        System.out.println('Implementing threads by inheriting the Thread class');
    }
}

The second method is to inherit the Thread class, as shown in the code. Unlike the first method, it does not implement an interface, but instead inherits the Thread class and overrides its run() method. I believe you are very familiar with these two methods and often use them in your work.

Creating threads with thread pools #

So why is it said that there is a third or fourth method? Let’s first look at the third method: creating threads with thread pools. Thread pools indeed provide an implementation of multithreading. For example, if we set the number of threads in the thread pool to 10, there will be 10 sub-threads working for us. Next, let’s delve into the source code of the thread pool to see how it implements 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 thread pools, they essentially create threads through thread factories. By default, they use the DefaultThreadFactory, which sets some default values for the threads created by the thread pool, such as the thread’s name, whether it is a daemon thread, and the thread’s priority, etc. However, regardless of how these properties are set, ultimately the threads are still created using new Thread(). It can be seen that creating threads with thread pools does not deviate from the two basic creation methods mentioned earlier, because it is essentially implemented through new Thread().

In interviews, if you only know that this method can create threads but do not understand its implementation principles, you will struggle in the interview process and inadvertently set a trap for yourself.

Therefore, when answering questions about thread implementation, after describing the first two methods, you can further say, “I also know that thread pools and Callable can create threads, but they are also implemented through the two basic methods mentioned earlier.” This kind of answer will earn you extra points in the interview. Then the interviewer is likely to follow up with questions about the composition and principles of thread pools, which will be analyzed in detail in later lessons.

Creating threads with Callable and returning a value #

class CallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }
}
}

// Create a thread pool

ExecutorService service = Executors.newFixedThreadPool(10);

// Submit a task and use Future to get the result

Future<Integer> future = service.submit(new CallableTask());

The fourth way to create threads is by using a Callable that returns a value. The Runnable interface creates threads without a return value, while the Callable interface and its related classes, such as Future and FutureTask, allow the thread execution result to be returned. As shown in the code, a class implementing the Callable interface is created, and the generic type is set to Integer. It will then return a random number.

However, whether using Callable or FutureTask, they are both tasks that need to be executed, similar to Runnable, and not threads themselves. They can be executed in a thread pool, as shown in the code, using the submit() method to submit the task to the thread pool, which will create the thread. Regardless of the method used, it ultimately relies on the thread to execute. The creation of sub-threads still follows the two basic ways mentioned at the beginning, which are implementing the Runnable interface and extending the Thread class.

Other ways to create threads #

Timer #

class TimerThread extends Thread {

    // Specific implementation

}

At this point, you might say that you know some other ways to create threads. For example, a timer can also be used to create a thread. If you create a Timer and set it to execute a task every 10 seconds or after two hours, it does indeed create a thread and execute the task. However, if we analyze the source code of the timer more deeply, we will find that fundamentally, it still has a TimerThread that extends the Thread class. Therefore, the creation of threads using a timer ultimately comes back to the two methods mentioned at the beginning.

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();
}
}

Perhaps you might say that there are other ways, such as anonymous inner classes or lambda expressions. However, creating threads using anonymous inner classes or lambda expressions is just implementing threads at the syntax level, and cannot be considered as different ways of implementing multithreading. For example, the code using anonymous inner classes simply creates an instance by using an anonymous inner class to pass in the Runnable object.

new Thread(() -> System.out.println(Thread.currentThread().getName())).start();

Let’s also look at the lambda expression approach. As shown in the code, ultimately they still fall under the two ways mentioned earlier.

There is only one way to implement threads #

First, let’s not focus on why there is only one way to create threads. Instead, let’s assume that there are two ways to create threads, and other ways, such as using thread pools or timers, simply encapsulate the new Thread() method. If we consider these as new ways of creating threads, then there would be countless ways to implement threads. For example, if the JDK updates and introduces new classes that encapsulate new Thread(), it would seem like a new way of implementing threads. By examining the essence after removing the encapsulation, we can see that ultimately they are all based on implementing the Runnable interface or extending the Thread class.

Next, let’s delve deeper into why these two ways are essentially the same.

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

Firstly, to start a thread, we need to call the start() method, which in turn calls the run() method. Let’s look at how the run() method is implemented in the first approach. We can see that the run() method is very short. In the first line of code, if (target != null), it checks if target is not null. If it’s not null, it executes the second line of code, target.run(), where target is actually a Runnable object passed in when implementing threads using the Runnable interface.

Now let’s look at the second approach, which is extending the Thread class. In this case, after extending the Thread class, we override the run() method. The overridden run() method directly contains the task that needs to be executed. However, we still need to call the thread.start() method to start the thread, and the start() method will eventually call the overridden run() method to execute the task. From this, we can completely understand that there is only one way to create threads, which is by constructing a Thread class instance. This is the only way to create threads.

We have already understood that the essence of the two ways of creating threads is the same, and the only difference lies in the content of thread execution. So where does the execution content come from?

The execution content mainly comes from two sources: either from target, or from the overridden run() method. Based on this, we can describe it as follows: essentially, there is only one way to implement threads, but there are two ways to specify the execution content. We can either use the Runnable interface by passing in the desired code or use the approach of extending the Thread class and overriding the run() method. Additionally, if we want more ways to implement threads, such as using thread pools and Timer schedulers, we only need to encapsulate the existing ways.

Implementing threads using the Runnable interface is preferred over extending the Thread class #

Now let’s compare the two ways of specifying the execution content, specifically why implementing the Runnable interface is preferred over extending the Thread class. What are the advantages?

Firstly, from the perspective of code architecture, the Runnable interface only has one run() method, which defines the content that needs to be executed. In this case, implementing Runnable and the Thread class are decoupled. The Thread class is responsible for thread startup and attribute setting, clearly separating responsibilities.

The second advantage is that it can improve performance in certain situations. When extending the Thread class, each time a task is executed, a new independent thread needs to be created. When the task is finished, the thread reaches the end of its lifecycle and gets destroyed. If we want to execute the same task again, we have to create another class that extends the Thread class. If the task contains only a small amount of code, such as printing a simple line of text in the run() method, the overhead it brings is relatively small. However, compared to the entire process of creating and destroying a thread, the overhead of these operations is much larger than the overhead of executing the code itself. It is not worth it. If we use the approach of implementing the Runnable interface, we can directly pass the task to a thread pool and use a fixed number of threads to complete the tasks, without creating and destroying threads each time, greatly reducing performance overhead.

The third advantage is that Java does not support multiple inheritance. If our class inherits the Thread class, it will not be able to inherit other classes in the future, limiting its extensibility for adding additional functionality. By using the Runnable interface, we avoid this restriction and ensure the future extensibility of our code.

To summarize, we should prioritize using the approach of implementing the Runnable interface to create threads.

Alright, that wraps up the content of this lesson. In this lesson, we mainly learned several ways to create threads, such as using the Runnable interface and extending the Thread class. We also analyzed why there is essentially only one way to implement threads and the advantages of implementing the Runnable interface over extending the Thread class. After completing this lesson, I believe you have a deeper understanding of creating threads.