Suddenly want to see the thread pool

Posted by Daleeburg on Wed, 04 Dec 2019 19:58:28 +0100

1 why to apply thread pool

   first of all, we know that thread is a precious resource for the operating system. For example, if we create it manually every time we use it, the thread will shut down automatically after executing the run() method, and we have to create it manually the next time we use it. This is a waste of time and resources for both the operating system and us, so we can choose to maintain some lines These threads continue to execute other received tasks after the completion of the tasks, so as to realize the reuse of resources. These threads constitute the usual thread pool. It can bring many benefits, such as:

  • Realize the reuse of thread resources. Reduce the waste of resources for manually shutting down threads.

  • Improve the response speed to a certain extent. Threads within the scope of thread pool can be directly used without manual creation.

  • Convenient for thread management. By putting threads together, we can set their state or timeout time in a unified way, so as to achieve our expected state. In addition, we can reduce the occurrence of OOM. For example, if we create threads in a certain place due to some misoperation, it will lead to system crash. However, if we use thread pool, we can set tasks to be executed at the same time The upper thread limit and the maximum number of tasks that can be received can well avoid this situation. (of course, this is based on your maximum number of threads and tasks in a reasonable range)

We know that in the thread life cycle (see another article about thread life cycle—— Java thread state and correct posture for thread shutdown ), the thread will enter the termination state after the normal execution of the run() method, so how does the thread pool realize that a thread does not enter the death state after the execution of a task and continues to perform other tasks?

How to realize thread reuse in thread pool

In fact, you can probably guess that a while loop is used to obtain task execution. Let's see how it is implemented internally. First, look at the execute() method:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
  
    // These fancy threads can be ignored. The logic is about the thread pool workflow in Section 3.1 below. The key is to find the thread start method
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        // The start method of its thread is put in the addWorker method here
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
   // some code....
}

Click here to see the implementation logic. Here, delete some logic to make it clearer:

private boolean addWorker(Runnable firstTask, boolean core) {
    // some code...
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // code...
            if (workerAdded) {
                /*
                 * We can see that the thread starts here. We find the root of t and find that it is the thread object in Worker
                 * And the execution we passed in is also passed into the worker
                 */
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        // ...
    }
    return workerStarted;
}

In the construction method of worker, we can see that thread is created by using thread factory, and the creation method passes itself in (inner class worker implements Runnable method)

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

That is to say, the runnable object in thread is the Worker itself. Calling the thread.start() method will call the Worker's run() method. Then using t.start() to start the thread above will call the Worker's run() method. Let's see its internal implementation.

// Call runWorker()
public void run() {
    runWorker(this);
}

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        try {
            // The while loop keeps getting the task execution from the queue until the condition is met to exit the loop
            while (task != null || (task = getTask()) != null) {
                w.lock();
                try {
                    // The default null implementation, which can be overridden to do something before the thread executes
                    beforeExecute(wt, task);
                    try {
                        // Call the run method of task directly, and task is the runnable we passed in
                        task.run();
                    } catch (Exception x) {
                        thrown = x; throw x;
                    }  finally {
                        // Ditto, hook method
                        afterExecute(task, thrown);
                    }
                } 
            }
         // other code
    }

  okay, here we know how the thread pool can realize task reuse after thread execution. It's similar to what we thought at the beginning that we use a while loop to get tasks from the queue, explicitly call the run() method of the task, until no queue is empty (or other error factors exit the loop).

3 how thread pools work

3.1 workflow of thread pool

First of all, the previous working diagram of the thread pool is used to understand the workflow of the thread pool according to the working diagram. Remembering this working diagram is very helpful for understanding the thread pool.

  1. When executing Executor.execute(runnable) or submit(runnable/callable), check whether the number of threads in the thread pool reaches the number of core threads. If not, create a 'core thread' to execute the task. (it can be understood that there are two types of threads in the thread pool: the 'core thread' and the 'maximum thread'. The 'maximum thread' will shut down itself after a period of idle time, and the 'core thread' will always try to get work.)
  2. If the number of core threads is reached, check whether the queue is full. If not, put the task into the queue for consumption. (tasks and threads in the online process pool do not interact directly. Generally, they maintain a blocking queue. When tasks come, they try to put them in the queue, while threads take tasks from the queue for execution.)
  3. If the queue is full, check whether the number of threads reaches the maximum number. If not, create a 'maximum thread' to execute the task. Otherwise, execute the rejection policy.

3.2 how to create a thread pool

                    .

public class Test {

    /* -----For ease of understanding, the types in the thread pool can be divided into two categories - [core thread] and [maximum thread]------ */
    /* -----You can directly look at the example of main method, and then look at the annotation of parameters here for easy understanding------ */    

    /**
     * The number of core threads. When the core thread of the thread pool reaches this value, the receiving task will no longer create threads,
     * Instead, put it in the queue and wait for consumption until the queue is full
     */
    private static final int CORE_POOL_SIZE = 1;

    /**
     * The maximum number of threads. When the queue is full and new tasks are received, a 'maximum thread' will be created to relieve the pressure,
     * 'The maximum thread 'will die after idle for a period of time. The specific idle time depends on the following keep ﹣ alive ﹣ time,
     * 'For example, the number of core threads is 1, and the maximum number of threads is 2,
     * Then the maximum number of core threads is 1, and the maximum number of 'maximum threads' is 1 (maximum number of threads - number of core threads)
     */
    private static final int MAXIMUM_POOL_SIZE = 2;

    /** The space time of the maximum thread. When the idle time of the 'maximum thread' reaches this value, it will die. The time unit is the next parameter, TimeUnit*/
    private static final int KEEP_ALIVE_TIME = 60;

    /** Unit of measurement of idle time*/
    private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;

    /**
     * Task queue is a blocking queue. The characteristics of blocking queue are
     *      1.When the take() method is called, if the queue is empty, it will enter the blocking state instead of returning null
     *      2.When the put() method is called, if the queue is full, it will enter the blocking state
     * The blocking queue can be divided into array queue and Linked queue (the difference is probably the difference between List and Linked). You can set
     * The boundary value is used to determine the maximum number of tasks in the queue. If it is exceeded, the maximum thread will be created or the rejection policy will be adopted
     * If the boundary value is set, it is a bounded queue, otherwise it is an unbounded queue (unbounded queue is easy to cause OOM, and the size of the queue should be determined according to the demand)
     */
    private static final BlockingQueue<Runnable> BLOCKING_QUEUE = new ArrayBlockingQueue<>(1);

    /** Thread factory, from which the thread executing the task is generated*/
    private static final ThreadFactory THREAD_FACTORY = Executors.defaultThreadFactory();

    /**
     * When the maximum thread is reached and the queue is full, there are four processing measures for new tasks, which are implemented by the ThreadPoolExecutor inner class
     *      1,ThreadPoolExecutor.CallerRunsPolicy The current thread performs its tasks, that is, the thread that calls executor.execute(),
                                                   Instead of thread pool providing additional thread execution
     *      2,ThreadPoolExecutor.AbortPolicy Throw RejectedExecutionException directly.
     *      3,ThreadPoolExecutor.DiscardPolicy Discard the task directly, do not process the new task, and do not get any feedback.
     *      4,ThreadPoolExecutor.DiscardOldestPolicy Discard the oldest task in the queue (refers to the first task in the queue, and call the poll() method to discard it),
                                                      Then call the executor.execute() method again
     */
    private static final RejectedExecutionHandler REJECTED_EXECUTION_HANDLER =  new ThreadPoolExecutor.AbortPolicy();

    public static void main(String[] args) throws InterruptedException {
        // Create a thread pool with 1 main threads, 2 maximum threads and 1 queue size
        ThreadPoolExecutor executor = new ThreadPoolExecutor(CORE_POOL_SIZE,
                MAXIMUM_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TIME_UNIT,
                BLOCKING_QUEUE,
                THREAD_FACTORY,
                REJECTED_EXECUTION_HANDLER);
        // At this time, there are no threads in the thread pool. Create a main thread to execute
        executor.execute(() -> {
            try {
                System.err.println("execute thread1:" + Thread.currentThread().getName());
                // Sleep for 1 second, verify that the thread below is put into the queue instead of creating the thread again
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // At this time, the number of main threads has reached the maximum, and new tasks are put into the queue
        executor.execute(() -> {
            try {
                System.err.println("execute thread2:" + Thread.currentThread().getName());
                // Sleep for 1 second
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // When receiving the task again, try to create the maximum number of threads to execute because the queue is full
        executor.execute(() -> {
            try {
                System.err.println("execute thread3:" + Thread.currentThread().getName());
                // Sleep for 1 second
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // The thread pool is already saturated. When the task comes again, the rejection policy is adopted. Here, a direct error is reported
        executor.execute(() -> {
            try {
                System.err.println("execute thread4:" + Thread.currentThread().getName());
                // Sleep for 1 second
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

Results:

                               from the execution order, it can be seen that 1 - > 3 - > 2 does not have 4, which is a good New tasks are executed instead of tasks in the queue head). At this time, the thread pool is saturated. If you want to handle new tasks according to the rejection policy, you can choose to report an error, so task 4 will not be executed.

For space reasons, there is no other rejection strategy. You can test it on your own machine if you want to verify.

3.3 talk about common thread pools of Executors

                     a framework of Executors is provided in the JUC package, which provides five ways to quickly create thread pools - newFixedThreadPool(), newsinglethreadexecution(), newCachedThreadPool(), newScheduledThreadPool(), and newWorkStealingPool() (new after Java 1.8, not introduced here.

  • newFixedThreadPool(int n): Create aBoth the number of core threads and the maximum number of threads are nIn other words, there are only core threads in the thread pool,There will be no maximum threads(Maximum thread capacity=Maximum threads-Number of core threads),The queue it uses is LinkedBlockingQueue Boundless queue means that no task will be abandonedPossible OOM,This is a major drawback.

    ​ It's OK to have a look at these. It's not necessary to remember or even suggest to forget that the essence of the first four kinds of rapid creation of thread pools is to use ThreadPoolExecutor It's justDifferent parametersIt's just like this. You can seeThese parameters determine its properties.

    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
  • newSingleThreadExecutor(): there is nothing to say in the implementation aspect, that is, the n of the above thread pool is 1, but the only difference is that there is another layer outside its implementation.

    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
  •   the function of this layer is to rewrite the finalize() method to ensure that the thread pool is closed when the JVM recycles. For finalization and JVM recycling, see another previous article JVM garbage collection Article.

    // Concrete realization
    static class FinalizableDelegatedExecutorService
            extends DelegatedExecutorService {
            FinalizableDelegatedExecutorService(ExecutorService executor) {
                super(executor);
            }
        
            // The finalize() method is rewritten to ensure that the rest of the tasks are executed first when the thread pool is recycled
            protected void finalize() {
                super.shutdown();
            }
        }
    • newCachedThreadPool(): the cache queue has no core thread, and the maximum number of threads is integer.max'value. It uses a special synchronousque queue, which does not occupy the storage space in essence. When a task is submitted to the thread pool, if there is no task to receive, it will enter a blocking state. The same is true when a thread takes a task from the queue The maximum number of threads passing here is integer. Max? Value, so if there is no idle thread, the thread will be created all the time, which may also cause OOM * *.
     public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          // Synchronous queue
                                          new SynchronousQueue<Runnable>());
        }
    • The new scheduledthreadpool (int n: timed thread pool, with n core threads and integer.max'value as the maximum number of threads, is implemented by DelayedWorkQueue, which is very suitable for the implementation of timed tasks.

       public ScheduledThreadPoolExecutor(int corePoolSize) {
              super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                    new DelayedWorkQueue());
          }

                             .

    [mandatory] thread pools are not allowed to be created by Executors, but by ThreadPoolExecutor. Such a processing method enables the students who write to be more clear about the running rules of thread pools and avoid the risk of resource exhaustion.

    4 Summary

                                      Process pool, effective management of these threads can reduce the loss and improve the response speed to a certain extent.

       we know that the thread will die after calling the start() method, so how does the thread pool realize the task execution all the time? In the execute() method, we found that the key to its implementation is the inner worker class. Worker is an internal class, which implements the Runnable interface. When a thread factory creates a thread, it will pass the worker itself in. When a thread calls the start() method, it will call the worker's run() method. In the worker's run() method, it uses a while loop to constantly get tasks from the queue, and then displays the run() method of executing tasks, so as to realize the goal Now a thread performs multiple tasks.

                              .

                                  . At the same time, it is also pointed out that there is a hidden danger of OOM when Executors create thread pools, so it is recommended to use ThreadPoolExecutor to create them.

    If the article is wrong, I hope you can point out.

    Even if I have no conscience, I would say, "Java is the best language in the world. "

    Topics: Java jvm REST