09 Where Is It Better to Use Thread Pools Over Manually Creating Threads

09 Where Is It Better to Use Thread Pools Over Manually Creating Threads #

In this lesson, we will mainly learn why using a thread pool is better than manually creating threads, and explain specifically why.

Why use a thread pool #

First of all, let’s review the relevant knowledge about thread pools. When Java was first created, there was no concept of a thread pool. Threads were created first, and as the number of threads increased, people realized that they needed a dedicated class to manage them, and that’s how thread pools were born. In the absence of a thread pool, a new thread had to be created for each task, which was not a problem when there were only a few tasks, as shown in the code.

/**
* Description: Execute a single task by creating a new thread manually
*/

public class OneTask {

    public static void main(String[] args) {

        Thread thread0 = new Thread(new Task());

        thread0.start();

    }

    static class Task implements Runnable {

        public void run() {

           System.out.println("Thread Name: " + Thread.currentThread().getName());

        }

    }

}

In this code, we create a new task and put it in a sub-thread, then start the sub-thread to execute the task. The task is very simple, it just prints the name of the current thread. In this case, the print result displays “Thread Name: Thread-0”, which is the default name of our current sub-thread.

img

Let’s take a look at the task execution process, as shown in the diagram. The main thread calls the start() method, and a sub-thread t0 is started. This is the scenario of a single task. As we have more tasks, for example, now we have 10 tasks, we can use a for loop to create 10 sub-threads, as shown in the code.

/**
* Description: Create 10 threads using a for loop
*/

public class TenTask {

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {

            Thread thread = new Thread(new Task());

            thread.start();

        }

    }

    static class Task implements Runnable {

        public void run() {

            System.out.println("Thread Name: " + Thread.currentThread().getName());

        }

    }

}

Execution result:

Thread Name: Thread-1
Thread Name: Thread-4
Thread Name: Thread-3
Thread Name: Thread-2
Thread Name: Thread-0
Thread Name: Thread-5
Thread Name: Thread-6
Thread Name: Thread-7
Thread Name: Thread-8
Thread Name: Thread-9

Here, you will find that the order of the printed results is chaotic, for example, Thread-4 is printed before Thread-3. This is because although Thread-3 calls the start method before Thread-4, it does not mean that Thread-3 will run first. The order of execution depends on the thread scheduler and has a lot of randomness, which is something we need to be aware of.

img

Let’s take another look at the execution process of the threads, as shown in the diagram. The main thread creates sub-threads t0~t9 through a for loop, and they can all execute the tasks normally. But what if our task suddenly surges to 10000? Let’s see how it will be implemented using a for loop:

for (int i = 0; i < 10000; i++) {

    Thread thread = new Thread(new Task());
``

thread.start();


```java
public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10000; i++) {
            service.execute(new Task());
        }

        System.out.println(Thread.currentThread().getName());
    }

    static class Task implements Runnable {
        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }
    }
}

As shown in the figure above, we created 10,000 sub-threads. In Java, each thread corresponds to a thread in the operating system. Assuming that the tasks in the threads take some time to complete, it will lead to a large system overhead and resource wastage.

Creating a thread incurs system overhead, and each thread also occupies a certain amount of memory and other resources. More importantly, creating so many threads can also harm stability, because in each system, there is a limit to the number of threads that can be created and it is not possible to create an infinite number. Threads need to be recycled after execution, and a large number of threads will put pressure on garbage collection. But we do have a lot of tasks. If they are all executed serially in the main thread, the efficiency will be too low. What should we do then? This is where the thread pool comes in to balance the relationship between threads and system resources.

Let’s summarize the problems of creating a thread for each task:

  1. First, repeatedly creating threads incurs a relatively high system overhead. Creating and destroying each thread takes time, and if the task is relatively simple, it is possible that the resources consumed by creating and destroying threads will be greater than the resources consumed by the threads themselves.
  2. Second, having too many threads will consume too much memory and other resources, and it will also lead to excessive context switching and an unstable system.

To solve the two problems mentioned above, thread pools have two solutions.

First, to address the problem of high overhead when repeatedly creating threads, a thread pool keeps a fixed number of threads in a working state and repeatedly executes tasks.

Second, to address the problem of excessive memory consumption caused by too many threads, the solution is more direct. The thread pool will create threads as needed and control the total number of threads to avoid excessive memory consumption.

How to use a thread pool #

A thread pool is like a pond, and the water in the pond is limited and controllable. For example, if we choose a thread pool with a fixed number of threads, let’s say 5, and at this time there are more than 5 tasks, the thread pool will make the remaining tasks queue up instead of unlimitedly increasing the number of threads, to ensure that resources are not excessively consumed. As shown in the code, we put 10,000 tasks into a thread pool with 5 threads and print the current thread name. What will the result be?

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10000; i++) {
            service.execute(new Task());
        }

        System.out.println(Thread.currentThread().getName());
    }

    static class Task implements Runnable {
        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }
    }
}

Execution result:

Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-2
Thread Name: pool-1-thread-3
Thread Name: pool-1-thread-4
Thread Name: pool-1-thread-5
Thread Name: pool-1-thread-5
Thread Name: pool-1-thread-5
Thread Name: pool-1-thread-5
Thread Name: pool-1-thread-5
Thread Name: pool-1-thread-2
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-5
Thread Name: pool-1-thread-3
Thread Name: pool-1-thread-5
...

As shown in the printed result, the thread name is continuously changing between “Thread Name: pool-1-thread-1” and “Thread Name: pool-1-thread-5”. It never exceeds this range, which proves that the thread pool does not exponentially increase the number of threads and always keeps these 5 threads working.

The execution flow is shown in the figure above. First, a thread pool with 5 threads is created. The thread pool assigns the 10,000 tasks to these 5 threads. These 5 threads repeatedly receive tasks and execute them until all tasks are completed. This is the idea of a thread pool.

The benefits of using a thread pool #

There are three main benefits to using a thread pool compared to manually creating threads.

  1. First, a thread pool can solve the system overhead problem of the thread lifecycle and speed up response time. Because the threads in the thread pool can be reused, we use a small number of threads to execute a large number of tasks, which greatly reduces the overhead of the thread lifecycle. Moreover, threads are usually not created only when tasks are received, they are already created and ready to execute tasks, eliminating the delay caused by thread creation and improving response time, enhancing user experience.
  2. Second, a thread pool can coordinate the use of memory and CPU resources, avoiding improper resource usage. The thread pool flexibly controls the number of threads according to the configuration and the number of tasks. If there are not enough threads, it creates new ones. If there are too many threads, it recycles them, avoiding an excessive number of threads leading to memory overflow or too few threads leading to CPU resource waste, achieving a perfect balance.
  3. Third, a thread pool can manage resources in a unified manner. For example, a thread pool can manage task queues and threads in a unified manner, it can start or stop tasks uniformly, it is more convenient and easier to manage than processing tasks one by one using individual threads, and it is also beneficial for data statistics. We can easily count the number of tasks that have been executed.