Thread pool code analysis

Before analyzing the source code of the thread pool, we must first know the role of the thread pool. The following points are quoted from "Code Efficient-Java Development Manual":

1 , using the thread pool manager thread and multiplexing, the control system and the maximum retransmission number and the like.

2 , to achieve the task thread queue caching strategies and rejection mechanism.

3 , to achieve some of the time-related functions, such as execution, execution cycle.

4. Isolate the thread environment. For example, if the transaction service and the search service are on the same server, two thread pools are opened separately, and the resource consumption of the transaction thread is obviously larger; therefore, by configuring an independent thread pool , the slower transaction service is isolated from the search service. Avoid each service thread from affecting each other.

 

So naturally the purpose of our use of thread pool is obvious:

1. Threads are scarce resources and cannot be created frequently.

2. Decoupling; the creation and execution of threads are completely separated, which is convenient for maintenance.

3. It should be put into a pool, which can be reused for other tasks.

Before looking at the thread pool source code, a picture should roughly remember that the solid arrow represents extends and the  dashed arrow represents implements :

After JDK1.5, related thread pool APIs were introduced. Our common ways to create thread pools are as follows:

  1. Executors.newCachedThreadPool() : unlimited thread pool.
  2. Executors.newFixedThreadPool(nThreads) : Create a fixed-size thread pool.
  3. Executors.newSingleThreadExecutor() : Create a thread pool of a single thread.

 

Looking at the code, you will find that, in fact, looking at the source code created by these three methods, you will find that the above three are implemented by using  ThreadPoolExecutor classes.

We learn how the thread pool creates threads. Let's start with the ThreadPoolExecutor construction method first

public ThreadPoolExecutor(    int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
}

The meaning of this core parameter:

  • corePoolSize is the basic size of the thread pool.
  • maximumPoolSize is the maximum thread size of the thread pool.
  • KeepAliveTime and unit are the survival time of the thread after it is idle.
  • workQueue is used to store the blocking queue of tasks.
  • Saturation strategy of handler when the queue and the maximum thread pool are full (add a friendly rejection strategy: ①Save to the database for peak shaving and valley filling, and then extract it for execution when it is free. ②Go to a prompt page. ③Print log ).
  • TimeUnit represents the time unit, the time unit of keepAliveTime is usually TimeUnit.SECONDS

Usually we use: threadPool.execute(new Job());

Let's write a simple thread pool first:

Step 1: Create a thread class for TaskDemo

import java.util.concurrent.TimeUnit;

public class TaskDemo implements Runnable {
    @Override
    public void run() {
        System.out.println("当前线程名:"+Thread.currentThread().getName()+"  is running "+ System.currentTimeMillis());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Step 2: Use the API interfaces of 3 common thread pools to create thread pools

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

public class ScheduledDemo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(5);
 //       ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Runnable task = new TaskDemo();
            pool.execute(task);
        }
    }
}

The execution result is: a time difference of 5 seconds, a fixed-size thread pool is created.

当前线程名:pool-1-thread-2  is running 1612099650398
当前线程名:pool-1-thread-1  is running 1612099650398
当前线程名:pool-1-thread-3  is running 1612099650398
当前线程名:pool-1-thread-4  is running 1612099650398
当前线程名:pool-1-thread-5  is running 1612099650399
当前线程名:pool-1-thread-2  is running 1612099655399
当前线程名:pool-1-thread-1  is running 1612099655399
当前线程名:pool-1-thread-3  is running 1612099655400
当前线程名:pool-1-thread-4  is running 1612099655400
当前线程名:pool-1-thread-5  is running 1612099655400

Seeing the execution of the thread pool, you know that the execute() function is very important.

Before the specific analysis, first understand the state defined in the thread pool, these states are closely related to the execution of the thread:

  

    private static final int RUNNING    = -1 << COUNT_BITS;

    private static final int SHUTDOWN   =  0 << COUNT_BITS;

    private static final int STOP       =  1 << COUNT_BITS;

    private static final int TIDYING    =  2 << COUNT_BITS;

    private static final int TERMINATED =  3 << COUNT_BITS;
private static final int COUNT_BITS = Integer.SIZE-3;

32-bit Integer to indicate the status: COUNT_BITS  29-bit thread count The upper 3 bits are the status code

  • RUNNING 

      (1) State description: Naturally, it is the running state, which means that the initialization state of the task thread pool in the task execution queue can be accepted as RUNNING. In other words, once the thread pool is created, it is in the RUNNING state, and the number of tasks in the thread pool is 0

      (2) State switching: the initialization state of the thread pool is RUNNING. In other words, once the thread pool is created, it is in the RUNNING state, and the number of tasks in the thread pool is 0!

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

  • SHUTDOWN 

      (1) State description: When the thread pool is in the SHUTDOWN state, it does not receive new tasks, but can process the added tasks.

     (2) State switching: When the shutdown() interface of the thread pool is called, the thread pool is changed from RUNNING -> SHUTDOWN.

  • STOP 
  1. State description: When the thread pool is in the STOP state, it does not receive new tasks, does not process the added tasks, and interrupts the tasks being processed.
  2. State switching: When the shutdownNow() interface of the thread pool is called, the thread pool is changed from (RUNNING or SHUTDOWN) -> STOP .
  • TIDYING 

(1) Status description: When all tasks have been terminated and the number of tasks is 0, the thread pool will become TIDYING. When the thread pool becomes TIDYING, the hook function terminated() will be executed. terminated() is in the ThreadPoolExecutor class The middle is empty. If the user wants to perform the corresponding processing when the thread pool becomes TIDYING; it can be achieved by overloading the terminated() function.

(2) State switching: When the thread pool is in the SHUTDOWN state, the blocking queue is empty and the tasks executed in the thread pool are also empty, it will be SHUTDOWN -> TIDYING. When the thread pool is in the STOP state, when the tasks executed in the thread pool are empty, it will be STOP -> TIDYING.

  • The TERMINATED  termination state  will be updated to this state when terminated() is executed .
  1. State description: The thread pool is completely terminated, and it becomes the TERMINATED state. 
  2. State switching: When the thread pool is in the TIDYING state, after the execution of terminated() , it will be TIDYING -> TERMINATED .

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();//获取当前线程池的状态 
        if (workerCountOf(c) < corePoolSize) {//当前线程数量小于 coreSize 时创建一个新的线程运行
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {//如果当前线程处于运行状态,并且写入阻塞队列成功
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))  //双重检查,再次获取线程状态;如果线程状态变了(非运行状态)就需要从阻塞队列移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
                reject(command);
            else if (workerCountOf(recheck) == 0)  //如果当前线程池为空就新创建一个线程并执行。
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) //如果在第三步的判断为非运行状态,尝试新建线程,如果失败则执行拒绝策略
            reject(command);
}

 The first time I wrote the thread pool, it felt a bit confusing. Finally, I added a few parameters of how to configure the core. One thing is for sure. The thread pool must be bigger, the better

Usually we need to determine according to the nature of this batch of tasks.

  • IO -intensive tasks: Since threads are not always running, you can configure as many threads as possible, such as the number of CPUs * 2
  • CPU -intensive tasks (a lot of complex calculations) should be allocated fewer threads, such as the size of the number of CPUs .

Of course, these are all empirical values, and the best way is to test the best configuration based on actual conditions.

 

Finally, remember:

Close the thread pool gracefully!

There are running tasks and naturally closing tasks. From the five states mentioned above, we can see how to close the thread pool.

In fact, it is nothing more than two methods shutdown()/shutdownNow().

But they have important differences:

  • After shutdown() is  executed, it stops accepting new tasks, and the tasks in the queue are executed.
  • shutdownNow()  also stops accepting new tasks, but will interrupt all tasks and change the thread pool state to stop .

The last and last: Thread pools are not allowed to use Executors, but are created by ThreadPoolExecutor. This processing method can more clearly define the running rules of the thread pool and avoid the risk of resource exhaustion.


Four public internal static classes are provided in ThreadPoolExecutor
• AbortPolicy (default): Abandon the task and throw a RejectedExecutionException.
• DiscardPolicy: Discard the task without throwing an exception. This is not recommended.
• DiscardOldestPolicy: Discard the longest waiting task in the queue, and then add the current task to the queue
.
• CallerRunsPolicy: Call the task's run() method to bypass the thread pool and execute directly.
 

import java.util.concurrent.atomic.AtomicLong;

public class Task implements Runnable {
    private final AtomicLong count= new AtomicLong(0L);
    @Override
    public void run() {
        System.out.println("running  "+ count.getAndIncrement());
    }
}
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class UserThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger nextId = new AtomicInteger(1);

    UserThreadFactory(String whatFeatureOfGroup) {
        namePrefix = "UserThreadFactory's "+whatFeatureOfGroup+" -Worker-";}

    @Override
    public Thread newThread(Runnable task){
            String name = namePrefix + nextId.getAndIncrement();
            Thread thread= new Thread(null,task,name,0);
            System.out.println(thread.getName());
            return thread;
    }
}
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class UserRejectHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("task rejected  "+ executor.toString());
    }
}
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class UserThreadPool {
    public static void main(String[] args ) {
        BlockingQueue queue = new LinkedBlockingQueue( 2 );
        UserThreadFactory fl = new UserThreadFactory ("第一号机房");
        UserThreadFactory f2 = new UserThreadFactory ("第一号机房");
        UserRejectHandler handler = new UserRejectHandler();
        ThreadPoolExecutor threadPoolFirst
                = new ThreadPoolExecutor(1,2,60 ,
                TimeUnit.SECONDS , queue , fl , handler);
        ThreadPoolExecutor threadPoolSecond
                = new ThreadPoolExecutor(1,2 , 60 ,
                TimeUnit . SECONDS , queue , f2 , handler) ;
        Runnable task= new Task();
        for (int i = 0; i < 200 ; i++) {
            threadPoolFirst.execute(task);
            threadPoolSecond.execute(task);
        }
    }
}
UserThreadFactory's 第一号机房 -Worker-1
UserThreadFactory's 第一号机房 -Worker-1
UserThreadFactory's 第一号机房 -Worker-2
UserThreadFactory's 第一号机房 -Worker-2
running  0
running  1
running  2
running  3
running  4
running  5
task rejected  java.util.concurrent.ThreadPoolExecutor@135fbaa4[Running, pool size = 2, active threads = 0, queued tasks = 0, completed tasks = 3]
task rejected  java.util.concurrent.ThreadPoolExecutor@45ee12a7[Running, pool size = 2, active threads = 0, queued tasks = 2, completed tasks = 3]

When a task is rejected, the rejection policy will print out that the size of the current thread pool has reached
maximumPoolSize=2, and the queue is full, indicating that there is already 1 completed task (the last line).

 

PS: The above part is quoted from "Code Efficient-Java Development Manual", non-commercial use, only for learning.

 

Guess you like

Origin blog.csdn.net/LB_Captain/article/details/113485079