[Java] Thread pool eight-part article one

Is the thread pool used in daily work? What is a thread pool? Why use thread pool?

As the face of the JUC package, the thread pool is truly the first brother of JUC. If you don’t understand the thread pool, it means you don’t know much about other tools in the JUC package. If you haven’t studied JUC in depth, you haven’t mastered it. The essence of Java gives the interviewer such an impression, and the results can be imagined.

It can be said that: with the development of computers to the present, 摩尔定律physical bottlenecks that are difficult to break through have been encountered under the current technological level. Improving server performance through multi-core CPU parallel computing has become mainstream and has emerged accordingly 多线程技术.

Threads are valuable resources of the operating system, and their use needs to be controlled and managed. Thread pools are tools that use pooling ideas (similar to connection pools, constant pools, object pools, etc.) to manage threads.

JUC provides the ThreadPoolExecutor system class to help us manage threads and execute tasks in parallel more conveniently.

The following figure is the Java thread pool inheritance system:

The top-level interface Executor provides a way to decouple the submission and execution of tasks. It only defines an execute(Runnable command) method to submit tasks. As for how to execute the specific tasks, it is left to its implementer to customize the implementation.

The ExecutorService interface inherits Executor and extends life cycle management methods, methods for returning Future, and methods for batch submission of tasks.

AbstractExecutorService abstract class inherits the ExecutorService interface and provides a default implementation for ExecutorService related methods. It uses RunnableFuture's implementation class FutureTask to wrap the Runnable task and hand it over to the execute() method for execution. Then you can block the execution results from the FutureTask and submit the batch tasks. Did the choreography.

ThreadPoolExecutor inherits AbstractExecutorService, uses the pooling idea to manage a certain number of threads to schedule and execute submitted tasks, and defines a set of life cycle states of the thread pool, using a ctl variable to simultaneously save the current pool state (high 3 bits) and the current pool. Number of threads (lower 29 bits). Friends who have read the source code will find that there are many methods in the ThreadPoolExecutor class that need to obtain or update the pool status and the current number of threads in the pool at the same time. Putting it in an atomic variable can ensure the consistency of the data and the simplicity of the code. When it comes to ctl, we can talk about the transfer process between several states.

 // 用此变量保存当前池状态(高3位)和当前线程数(低29位)
  private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); 
  private static final int COUNT_BITS = Integer.SIZE - 3;
  private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

  // runState is stored in the high-order bits
  // 可以接受新任务提交,也会处理任务队列中的任务
  // 结果:111跟29个0:111 00000000000000000000000000000
  private static final int RUNNING    = -1 << COUNT_BITS;
  
  // 不接受新任务提交,但会处理任务队列中的任务
  // 结果:000 00000000000000000000000000000
  private static final int SHUTDOWN   =  0 << COUNT_BITS;
  
  // 不接受新任务,不执行队列中的任务,且会中断正在执行的任务
  // 结果:001 00000000000000000000000000000
  private static final int STOP       =  1 << COUNT_BITS;
  
  // 任务队列为空,workerCount = 0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()
  // 结果:010 00000000000000000000000000000
  private static final int TIDYING    =  2 << COUNT_BITS;
  
  // 调用terminated()钩子方法后进入TERMINATED状态
  // 结果:010 00000000000000000000000000000
  private static final int TERMINATED =  3 << COUNT_BITS;

  // Packing and unpacking ctl
  // 低29位变为0,得到了线程池的状态
  private static int runStateOf(int c)     {
    
     return c & ~CAPACITY; }
  // 高3位变为为0,得到了线程池中的线程数
  private static int workerCountOf(int c)  {
    
     return c & CAPACITY; }
  private static int ctlOf(int rs, int wc) {
    
     return rs | wc; }

Using a thread pool can bring the following benefits:

1. Reduce resource consumption. Reduce the additional overhead caused by frequent thread creation and destruction, and reuse created threads;
2. Reduce usage complexity. To decouple task submission and execution, we only need to create a thread pool and submit tasks into it. 3. The specific execution process is managed by the thread pool itself, reducing usage complexity; 4. Improve thread manageability
. It can safely and effectively manage thread resources and avoid the risk of resource exhaustion caused by unlimited applications without restrictions;
5. Improve response speed. After the task arrives, the created thread is directly reused for execution.

In simple terms, the usage scenarios of thread pools can include:

1. Respond quickly to user requests, with priority given to response speed. For example, a user request needs to call several services through RPC to obtain data and then aggregate and return it. This scenario can be called in parallel using a thread pool. The response time depends on the time taken by the RPC interface with the slowest response; or a registration request. After registration, SMS and email notifications need to be sent. In order to quickly return to the user, the notification operation can be thrown into the thread pool for asynchronous execution, and then directly returned to the client successfully to improve user experience; 2. Process more requests per unit time
. Throughput takes precedence. For example, if you accept MQ messages and then call a third-party interface to query data, this scenario does not pursue fast response. It mainly uses limited resources to process as many tasks as possible in a unit of time. Queues can be used to buffer tasks.

Based on the above usage scenarios, you can apply it to your own project. In order to improve system performance, tell me what optimizations you have made on the thread pool of the system module you are responsible for. Compare the Qps improvement, Rt reduction, server number reduction, etc. before and after optimization. .

What are the core parameters of ThreadPoolExecutor?

Try to be familiar with the thread pool execution process.

You can’t just master the following :

Contains 7 parameters: core thread number (corePoolSize), maximum thread number (maximumPoolSize), idle thread timeout (keepAliveTime), time unit (unit), blocking queue (workQueue), rejection strategy (handler), and thread factory (ThreadFactory) .

It also requires a deeper understanding of :

I will take the initiative to describe the execution process of the thread pool, that is, the execute() method execution process.

The execution logic of the execute() method is as follows:

public void execute(Runnable command) {
    
    
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
    
    
        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 main execution process can be summarized as follows. Of course, there will be some abnormal branch judgments when looking at the above code. You can sort it out and add it to the main execution process below.

1. Determine the status of the thread pool. If it is not in the RUNNING state, directly execute the rejection policy;
2. If the current number of threads < core thread pool, create a new thread to process the submitted task;
3. If the current number of threads > the number of core threads and If the task queue is not full, put the task into the blocking queue and wait for execution;
4. If the core thread pool < the current number of thread pools < the maximum number of threads, and the task queue is full, create a new thread to execute the submitted task; 5.
If If the current number of threads > the maximum number of threads and the queue is full, the rejection policy will be executed to reject the task.

You should also understand the usage scenarios :

The execution process is mainly used in CPU-intensive scenarios.

In frameworks such as Tomcat and Dubbo, their internal thread pools are mainly used to handle network IO tasks, so they have adjusted the execution process of the JUC thread pool to support IO-intensive scenarios.

They provide a blocking queue TaskQueue, which inherits LinkedBlockingQueue and overrides the offer() method to adjust the execution process.

@Override
    public boolean offer(Runnable o) {
    
    
        // we can't do any checks
        if (parent==null) return super.offer(o);
        // we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        // we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
        // if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        // if we reached here, we need to add it to the queue
        return super.offer(o);
    }

You can see that he made several judgments before joining the team. The parent here is the thread pool object to which it belongs.

1. If parent is null, directly call the parent class offer method to join the queue;
2. If the current number of threads is equal to the maximum number of threads, directly call the parent class offer() method to join the queue;
3. If the number of currently unexecuted tasks is less than or equal to The current number of threads. If you think about it carefully, does it mean that there are idle threads? Then directly call the parent class offer() and there will be threads to execute it immediately after joining the queue; 4. If the
current number of threads is less than the maximum number of threads, return directly. false, then return to the execution process of the JUC thread pool and think about it, should you add a new thread to perform the task?
5. In other cases, join the queue directly.

It can be seen that when the current number of threads is greater than the number of core threads, the JUC native thread pool first puts tasks in the queue to wait for execution, instead of creating threads for execution first.

If the number of requests received by Tomcat is greater than the number of core threads, the requests will be placed in the queue and wait for core thread processing. If the concurrency is large, a large number of tasks will be accumulated in the queue, which will reduce the overall response speed of the request.

Therefore, Tomcat does not use the JUC native thread pool. It uses the offer() method of TaskQueue to cleverly modify the execution process of the JUC thread pool. After the modification, the Tomcat thread pool execution process is as follows:

1. Determine if the current number of threads is less than the core thread pool, then create a new thread to process the submitted task;
2. If the current number of thread pool is greater than the core thread pool and less than the maximum number of threads, create a new thread to execute the submitted task;
3. If the current number of threads is equal to the maximum number of threads, put the task into the task queue and wait for execution;
4. If the queue is full, execute the rejection policy.

Moreover, Tomcat will preheat the core thread. After creating the thread pool, it will create the core thread and start it. After the service is started, it can directly accept client requests for processing, avoiding the cold start problem.

Then let’s talk about the Worker thread model of the thread pool, which inherits AQS to implement the lock mechanism. After the thread is started, the runWorker() method is executed. The getTask() method is called in the runWorker() method to obtain the task from the blocking queue. After obtaining the task, the beforeExecute() hook function is executed first, then the task is executed, and then the afterExecute() hook function is executed. . If the task cannot be obtained after a timeout, the processWorkerExit() method will be called to clean up the Worker thread.

runworker()、getTask()、addWorker()

I just mentioned that Worker inherits AQS and implements the lock mechanism. So what locks does ThreadPoolExecutor use? Why use a lock?

1) mainLock lock

ThreadPoolExecutor maintains the ReentrantLock type lock mainLock internally. This reentrant lock needs to be obtained when accessing workers member variables and related data statistics and accounting (such as accessing largestPoolSize, completedTaskCount).

Why do we need mainLock ?

    private final ReentrantLock mainLock = new ReentrantLock();

    /**
     * Set containing all worker threads in pool. Accessed only when
     * holding mainLock.
     */
    private final HashSet<Worker> workers = new HashSet<Worker>();

    /**
     * Tracks largest attained pool size. Accessed only under
     * mainLock.
     */
    private int largestPoolSize;

    /**
     * Counter for completed tasks. Updated only on termination of
     * worker threads. Accessed only under mainLock.
     */
    private long completedTaskCount;

You can see that the HashSet used in the workers variable is thread-unsafe and cannot be used in a multi-threaded environment. largestPoolSize and completedTaskCount are also not modified with volatile, so they need to be accessed under the protection of a lock.

Why not just use a thread-safe container ?

In fact, Mr. Doug explained it in the comments of the mainLock variable, which means that it turns out that compared to thread-safe containers, lock is more suitable here. One of the main reasons is to serialize the interruptIdleWorkers() method to avoid unnecessary Interrupt storm.

How to understand this interruption storm ?

In fact, a simple understanding is that if the interruptIdleWorkers() method is not locked, this will happen under multi-thread access. A thread calls the interruptIdleWorkers() method to interrupt the Worker. At this time, the Worker is in the interrupted state. At this time, another thread comes to interrupt the Worker thread that is being interrupted. This is the so-called interrupt storm.

Then adding the volatile keyword to the largestPoolSize and completedTaskCount variables can eliminate the need for mainLock ?

Some other internal variables that can be used as volatile have been modified with volatile. The main reason for not adding these two is to ensure the accuracy of these two parameters. When obtaining these two values, it can be guaranteed that what is obtained must be the modification method execution. The value after completion. If the lock is not locked, the value may be obtained before the execution of the modification method is completed. What is obtained is the value before modification. Then once the modification method is submitted, the obtained data will be inaccurate.

2) Worker thread lock

As mentioned just now, the Worker thread inherits AQS, implements the Runnable interface, and internally holds three member variables: a Thread variable, a firstTask, and completedTasks.

Acquire() and tryAcquire() based on AQS implement the lock() and tryLock() methods, and there are also comments on the class. The lock is mainly used to maintain the interruption status of the running thread. It is used in the runWorker() method and the interruptIdleWorkers() method just mentioned.

How to understand the interruption status of this maintenance thread?

 protected boolean tryAcquire(int unused) {
    
    
      if (compareAndSetState(0, 1)) {
    
    
          setExclusiveOwnerThread(Thread.currentThread());
          return true;
      }
      return false;
  }
  public void lock()        {
    
     acquire(1); }
  public boolean tryLock()  {
    
     return tryAcquire(1); }

Before obtaining the task in the runWorker() method and starting to execute it, you need to call the w.lock() method first. The lock() method will call the tryAcquire() method. tryAcquire() implements a non-reentrant lock and implements the addition through CAS. Lock.

    protected boolean tryAcquire(int unused) {
    
    
          if (compareAndSetState(0, 1)) {
    
    
              setExclusiveOwnerThread(Thread.currentThread());
              return true;
          }
          return false;
      }
final void runWorker(ThreadPoolExecutor.Worker w) {
    
    
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
    
    
        while (task != null || (task = getTask()) != null) {
    
    
            w.lock(); // 需要先调用 w.lock() 方法
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                            runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                wt.interrupt();
            try {
    
    
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
    
    
                    task.run();
                } catch (RuntimeException x) {
    
    
                    thrown = x; throw x;
                } catch (Error x) {
    
    
                    thrown = x; throw x;
                } catch (Throwable x) {
    
    
                    thrown = x; throw new Error(x);
                } finally {
    
    
                    afterExecute(task, thrown);
                }
            } finally {
    
    
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
    
    
        processWorkerExit(w, completedAbruptly);
    }
}

The interruptIdleWorkers() method will interrupt those threads waiting to acquire tasks, and will call the w.tryLock() method to lock. If a thread is already executing a task, tryLock() will fail to acquire the lock, ensuring that the running process cannot be interrupted. of thread.

private void interruptIdleWorkers(boolean onlyOne) {
    
    
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
    
    
        for (ThreadPoolExecutor.Worker w : workers) {
    
    
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
    
    
                try {
    
    
                    t.interrupt();
                } catch (SecurityException ignore) {
    
    
                } finally {
    
    
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
    
    
        mainLock.unlock();
    }
}

How to use thread pool in the project? Do Executors understand?

Most companies now follow Alibaba's Java development specifications, which clearly state that it is not allowed to use Executors to create thread pools, but to create thread pools through ThreadPoolExecutor to display specified parameters.

The thread pool created by Executors is at risk of OOM.

The thread pool created by Executors.newFixedThreadPool and Executors.SingleThreadPool internally uses an unbounded (Integer.MAX_VALUE) LinkedBlockingQueue queue, which may accumulate a large number of requests, causing OOM.

The maximum number of threads in the thread pool created by Executors.newCachedThreadPool and Executors.scheduledThreadPool is Integer.MAX_VALUE, which may create a large number of threads, causing OOM.

I also encapsulate similar tool classes in my daily work, but they are all memory-safe. The parameters need to be specified by yourself. Appropriate values ​​are also implemented based on LinkedBlockingQueue. MemorySafeLinkedBlockingQueue is implemented. When the system memory reaches the set maximum remaining threshold, it will No more tasks are added to the queue to avoid OOM.

package cn.com.codingce.juc.thread;


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * 线程池工具类
 */
public class ThreadPoolUtils {
    
    

    /**
     * 线程池
     */
    private final ThreadPoolExecutor executor;

    /**
     * 线程工厂
     */
    private CustomThreadFactory threadFactory;

    /**
     * 异步执行结果
     */
    private final List<CompletableFuture<Void>> completableFutures;

    /**
     * 拒绝策略
     */
    private final CustomAbortPolicy abortPolicy;

    /**
     * 失败数量
     */
    private final AtomicInteger failedCount;

    public ThreadPoolUtils(int corePoolSize, int maximumPoolSize, int queueSize, String poolName) {
    
    
        this.failedCount = new AtomicInteger(0);
        this.abortPolicy = new CustomAbortPolicy();
        this.completableFutures = new ArrayList<>();
        this.threadFactory = new CustomThreadFactory(poolName);
        this.executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueSize), this.threadFactory, abortPolicy);
    }

    /**
     * 执行任务
     */
    public void execute(Runnable runnable) {
    
    
        CompletableFuture<Void> future = CompletableFuture.runAsync(runnable, executor);
        // 设置好异常情况
        future.exceptionally(e -> {
    
    
            failedCount.incrementAndGet();
            System.out.println("Task Failed..." + e);
            e.printStackTrace();
            return null;
        });
        // 任务结果列表
        completableFutures.add(future);
    }

    /**
     * 执行自定义runnable接口(可省略,只是加了个获取taskName)
     */
    public void execute(SimpleTask runnable) {
    
    
        CompletableFuture<Void> future = CompletableFuture.runAsync(runnable, executor);
        // 设置好异常情况
        future.exceptionally(e -> {
    
    
            failedCount.incrementAndGet();
            System.out.println("Task [" + runnable.taskName + "] Failed..." + e);
            e.printStackTrace();
            return null;
        });
        // 任务结果列表
        completableFutures.add(future);
    }

    /**
     * 停止线程池
     */
    public void shutdown() {
    
    
        executor.shutdown();
        System.out.println("************************停止线程池************************");
        System.out.println("** 活动线程数:" + executor.getActiveCount() + "\t\t\t\t\t\t\t\t\t\t**");
        System.out.println("** 等待任务数:" + executor.getQueue().size() + "\t\t\t\t\t\t\t\t\t\t**");
        System.out.println("** 完成任务数:" + executor.getCompletedTaskCount() + "\t\t\t\t\t\t\t\t\t\t**");
        System.out.println("** 全部任务数:" + executor.getTaskCount() + "\t\t\t\t\t\t\t\t\t\t**");
        System.out.println("** 拒绝任务数:" + abortPolicy.getRejectCount() + "\t\t\t\t\t\t\t\t\t\t**");
        System.out.println("** 成功任务数:" + (executor.getCompletedTaskCount() - failedCount.get()) + "\t\t\t\t\t\t\t\t\t\t**");
        System.out.println("** 异常任务数:" + failedCount.get() + "\t\t\t\t\t\t\t\t\t\t**");
        System.out.println("**********************************************************");
    }

    /**
     * 获取任务执行情况
     * 之所以遍历taskCount数的CompletableFuture,是因为如果有拒绝的任务,相应的CompletableFuture也会放进列表,而这种CompletableFuture调用get方法,是会永远阻塞的。
     */
    public boolean getExecuteResult() {
    
    
        // 任务数,不包含拒绝的任务
        long taskCount = executor.getTaskCount();
        for (int i = 0; i < taskCount; i++) {
    
    
            CompletableFuture<Void> future = completableFutures.get(i);
            try {
    
    
                // 获取结果,这个是同步的,目的是获取真实的任务完成情况
                future.get();
            } catch (InterruptedException | ExecutionException e) {
    
    
                System.out.println("java.util.concurrent.CompletableFuture.get() Failed ..." + e);
                return false;
            }
            // 出现异常,false
            if (future.isCompletedExceptionally()) {
    
    
                return false;
            }
        }
        return true;
    }

    /**
     * 线程工厂
     */
    private static class CustomThreadFactory implements ThreadFactory {
    
    

        private final String poolName;
        private final AtomicInteger count;

        private CustomThreadFactory(String poolName) {
    
    
            this.poolName = poolName;
            this.count = new AtomicInteger(0);
        }

        @Override
        public Thread newThread(Runnable r) {
    
    
            Thread thread = new Thread(r);
            // 线程名,利于排查
            thread.setName(poolName + "-[线程" + count.incrementAndGet() + "]");
            return thread;
        }
    }

    /**
     * 自定义拒绝策略
     */
    private static class CustomAbortPolicy implements RejectedExecutionHandler {
    
    
        /**
         * 拒绝的任务数
         */
        private final AtomicInteger rejectCount;

        private CustomAbortPolicy() {
    
    
            this.rejectCount = new AtomicInteger(0);
        }

        private AtomicInteger getRejectCount() {
    
    
            return rejectCount;
        }

        /**
         * 这个方法,如果不抛异常,则执行此任务的线程会一直阻塞
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            System.out.println("Task " + r.toString() + " rejected from " + e.toString() + " 累计:" + rejectCount.incrementAndGet());
        }
    }

    /**
     * 只是加了个taskName,可自行实现更加复杂的逻辑
     */
    public abstract static class SimpleTask implements Runnable {
    
    
        /**
         * 任务名称
         */
        private String taskName;

        public void setTaskName(String taskName) {
    
    
            this.taskName = taskName;
        }
    }

}

test:

package cn.com.codingce.juc.thread;


import java.util.Random;


/**
 * 线程池工具类
 */
public class ThreadPoolUtilsMain {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        test();
    }

    public static void test() throws InterruptedException {
    
    
        ThreadPoolUtils pool = new ThreadPoolUtils(5, 5, 10, "A业务线程池");
        // 14个正常任务
        for (int i = 0; i < 14; i++) {
    
    
            pool.execute(new ThreadPoolUtils.SimpleTask() {
    
    
                @Override
                public void run() {
    
    
                    try {
    
    
                        // 模拟任务耗时
                        Thread.sleep(600);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    // 随机名称
                    String taskName = randomName();
                    super.setTaskName(taskName);
                    System.out.println(Thread.currentThread().getName() + "-执行【" + taskName + "】");
                }
            });
        }
        // 1个异常任务
        pool.execute(new ThreadPoolUtils.SimpleTask() {
    
    
            @Override
            public void run() {
    
    
                // 随机名称
                String taskName = randomName();
                super.setTaskName(taskName);
                throw new RuntimeException("执行【" + taskName + "】" + "异常");
            }
        });
        // 5个多余用来拒绝的任务
        for (int i = 0; i < 5; i++) {
    
    
            pool.execute(new ThreadPoolUtils.SimpleTask() {
    
    
                @Override
                public void run() {
    
    
                    // 随机名称
                    String taskName = randomName();
                    super.setTaskName(taskName);
                    throw new RuntimeException("多余任务");
                }
            });
        }
        System.out.println("任务完成情况:" + pool.getExecuteResult());

        pool.shutdown();

        Thread.sleep(20000);
    }

    private static String randomName() {
    
    
        return "任务" + (char) (new Random().nextInt(60) + 65);
    }

}

output

Task java.util.concurrent.CompletableFuture$AsyncRun@378bf509 rejected from java.util.concurrent.ThreadPoolExecutor@5fd0d5ae[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0] 累计:1
Task java.util.concurrent.CompletableFuture$AsyncRun@2d98a335 rejected from java.util.concurrent.ThreadPoolExecutor@5fd0d5ae[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0] 累计:2
Task java.util.concurrent.CompletableFuture$AsyncRun@16b98e56 rejected from java.util.concurrent.ThreadPoolExecutor@5fd0d5ae[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0] 累计:3
Task java.util.concurrent.CompletableFuture$AsyncRun@7ef20235 rejected from java.util.concurrent.ThreadPoolExecutor@5fd0d5ae[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0] 累计:4
Task java.util.concurrent.CompletableFuture$AsyncRun@27d6c5e0 rejected from java.util.concurrent.ThreadPoolExecutor@5fd0d5ae[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0] 累计:5
A业务线程池-[线程1]-执行【任务{
    
    】
A业务线程池-[线程4]-执行【任务H】
A业务线程池-[线程2]-执行【任务r】
A业务线程池-[线程5]-执行【任务N】
A业务线程池-[线程3]-执行【任务O】
A业务线程池-[线程1]-执行【任务[】
A业务线程池-[线程5]-执行【任务n】
A业务线程池-[线程4]-执行【任务M】
A业务线程池-[线程3]-执行【任务V】
A业务线程池-[线程2]-执行【任务n】
Task [任务J] Failed...java.util.concurrent.CompletionException: java.lang.RuntimeException: 执行【任务J】异常
java.util.concurrent.CompletionException: java.lang.RuntimeException: 执行【任务J】异常
	at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
	at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
	at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1629)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.RuntimeException: 执行【任务J】异常
	at cn.com.codingce.juc.thread.ThreadPoolUtilsMain$2.run(ThreadPoolUtilsMain.java:43)
	at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1626)
	... 3 more
A业务线程池-[线程1]-执行【任务Q】
A业务线程池-[线程3]-执行【任务C】
A业务线程池-[线程4]-执行【任务q】
A业务线程池-[线程5]-执行【任务t】
java.util.concurrent.CompletableFuture.get() Failed ...java.util.concurrent.ExecutionException: java.lang.RuntimeException: 执行【任务J】异常
任务完成情况:false
************************停止线程池************************
** 活动线程数:0										**
** 等待任务数:0										**
** 完成任务数:15										**
** 全部任务数:15										**
** 拒绝任务数:5										**
** 成功任务数:14										**
** 异常任务数:1										**
**********************************************************

We generally use thread pools in the Spring environment. There is a problem with using JUC's native ThreadPoolExecutor directly. When the Spring container is closed, the tasks in the task queue may not be processed yet, and there is a risk of losing tasks.

We know that Beans in Spring have a life cycle. If a Bean implements Spring's corresponding life cycle interface (InitializingBean, DisposableBean interface), the corresponding method will be called for corresponding processing when the Bean is initialized and the container is closed.

So it is best not to use ThreadPoolExecutor directly in the Spring environment. You can use the ThreadPoolTaskExecutor provided by Spring .

Thread pools are also isolated according to business types. The execution of each task does not affect each other. This avoids sharing a thread pool. Task execution is uneven and affects each other. High-time-consuming tasks will occupy the thread pool resources, resulting in low-time-consuming tasks having no chance. Execution; at the same time, if there is a parent-child relationship between tasks, it may cause deadlock, which may lead to OOM.

The conventional operation of using a thread pool is to define multiple business-isolated thread pool instances through @Bean. We made a dynamically monitoring thread pool wheel based on the article on Meituan Thread Pool Practice, and took advantage of some features of Spring to configure the thread pool instances in the configuration center. When the service starts, it will be pulled from the configuration center. Get the configuration and then generate a BeanDefinition and register it in the Spring container. When the Spring container is refreshed, a thread pool instance will be generated and registered in the Spring container. In this way, our business code does not need to explicitly declare the thread pool with @Bean. We can directly use the thread pool through dependency injection, and we can also dynamically adjust the parameters of the thread pool.

@EnableAsync
@Configuration
public class ThreadPoolConfig {
    
    
	@Bean
	public ThreadPoolTaskExecutor excelExecutor() {
    
    
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setCorePoolSize(5);
		executor.setMaxPoolSize(10);
		executor.setQueueCapacity(100000);
		executor.setKeepAliveSeconds(60);
		executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
		executor.setThreadNamePrefix("excel-");
		return executor;
	}
}

application

@Resource
ThreadPoolTaskExecutor excelExecutor;

// ...
List<CompletableFuture<List<SimplePeOnlineLabelDto>>> list = new ArrayList<>();

List<List<String>> reportList = ListUtil.split(reportIds, 200);
reportList.forEach(ls -> {
    
    
    CompletableFuture<List<SimplePeOnlineLabelDto>> completableFuture = CompletableFuture.supplyAsync(() -> peOnlineDataService.getPeOnlineReptLabelData(ls),excelExecutor);
    list.add(completableFuture);
});

for (CompletableFuture<List<SimplePeOnlineLabelDto>> listCompletableFuture : list) {
    
    
    log.info("========================读取数据=========================");
    try {
    
    
        List<SimplePeOnlineLabelDto> simplePeOnlineLabelDtoList = listCompletableFuture.get();
        peOnlineData.addAll(simplePeOnlineLabelDtoList);
    }catch (Exception e){
    
    
        log.error("========================读取数据失败=========================");
    }
}
// ...

To create a thread pool through ThreadPoolExecutor, what is the appropriate core parameter setting?

Many people may have seen a thread number calculation formula introduced in the book "Java Concurrent Programming Practice":

Ncpu = number of CPU cores
Ucpu = target CPU utilization, 0 <= Ucpu <= 1
W / C = waiting time / calculation time.
To run the program to the target utilization of the CPU, the number of threads required is:
Nthreads = Ncpu * Ucpu * (1+W/C)

This formula is too theoretical and difficult to implement in practice. First of all, it is difficult to obtain accurate waiting time and calculation time. There will be many threads running in a service. For example, Tomcat has its own thread pool, Dubbo has its own thread pool, and GC also has its own background thread. The various frameworks and middleware we introduce may have their own working threads. , these threads will occupy CPU resources, so the error calculated by this formula must be large.

So how to determine the thread pool size?

In fact, there is no fixed answer. 需要通过压测不断的动态调整线程池参数, 观察 CPU 利用率、系统负载、GC、内存、RT、吞吐量etc. 综合指标数据, to find one 相对比较合理的值.

So don’t ask how many threads are appropriate to set. There is no standard answer to this question. You need to set a series of data indicators based on the business scenario, eliminate possible interference factors, pay attention to link dependencies (such as connection pool restrictions, three-party interface current limit), and then By constantly dynamically adjusting the number of threads, the test finds a relatively suitable value.

How is the thread pool monitored?

Because the operation of the thread pool is relatively a black box, we cannot perceive its operation. This question mainly examines how to perceive the operation of the thread pool.
Made some enhancements to the thread pool ThreadPoolExecutor and created a thread pool management framework. The main functions include monitoring alarms and dynamic parameter adjustment. It mainly uses some set, get methods and some hook functions provided by the ThreadPoolExecutor class.
Dynamic parameter adjustment is implemented based on the configuration center. The core parameters are configured in the configuration center and can be adjusted at any time and take effect in real time, using the set method provided by the thread pool.

Monitoring mainly uses some get methods provided by the thread pool to obtain some indicator data, and then collects the data and reports it to the monitoring system for market display. Endpoint also provides real-time viewing of thread pool indicator data.

At the same time, 5 alarm rules are defined.

Thread pool activity alarm. Activity = activeCount / maximumPoolSize. When the activity reaches the configured threshold, a prior alarm will be issued; a
queue capacity alarm will be issued. Capacity usage = queueSize / queueCapacity. When the queue capacity reaches the configured threshold, a prior alarm will be issued;
policy alarms will be rejected. When the rejection policy is triggered, an alarm will be issued;
task execution timeout alarm will be issued. Rewrite afterExecute() and beforeExecute() of ThreadPoolExecutor, and calculate the task execution duration based on the difference between the current time and the start time. If the configured threshold is exceeded, an alarm will be triggered; task queuing timeout alarm will occur
. Rewrite beforeExecute() of ThreadPoolExecutor, record the time when the task is submitted, and calculate the task queue time based on the difference between the current time and the submission time. If the configured threshold is exceeded, an alarm will be triggered.

Through monitoring + alarms, we can promptly perceive the execution load of our business thread pool and make adjustments as soon as possible to prevent accidents.

What is the difference between executing() to submit a task and submit() to submit a task?

Seeing this question, do most people think I can do this? execute() has no return value, submit() has a return value and will return a FutureTask, and then the get() method can be called to block and obtain the return value.

This question mainly involves the implementation principle of FutureTask. The inheritance system of FutureTask is as follows:

The task (Runnable or Callable) submitted by calling the submit() method will be packaged into a FutureTask() object. The FutureTask class provides 7 task states and five member variables.

 /*
     * Possible state transitions:
     * NEW -> COMPLETING -> NORMAL
     * NEW -> COMPLETING -> EXCEPTIONAL
     * NEW -> CANCELLED
     * NEW -> INTERRUPTING -> INTERRUPTED
     */
    // 构造函数中 state 置为 NEW,初始态
    private static final int NEW          = 0;
    // 瞬时态,表示完成中
    private static final int COMPLETING   = 1;
    // 正常执行结束后的状态
    private static final int NORMAL       = 2;
    // 异常执行结束后的状态
    private static final int EXCEPTIONAL  = 3;
    // 调用 cancel 方法成功执行后的状态
    private static final int CANCELLED    = 4;
    // 瞬时态,中断中
    private static final int INTERRUPTING = 5;
    // 正常执行中断后的状态
    private static final int INTERRUPTED  = 6;

    // 任务状态,以上 7 种
    private volatile int state;
    /** 通过 submit() 提交的任务,执行完后置为 null*/
    private Callable<V> callable;
    /** 任务执行结果或者调用 get() 要抛出的异常*/
    private Object outcome; // non-volatile, protected by state reads/writes
    /** 执行任务的线程,会在 run() 方法中通过 cas 赋值*/
    private volatile Thread runner;
    /** 调用get()后由等待线程组成的无锁并发栈,通过 cas 实现无锁*/
    private volatile WaitNode waiters;

When creating a FutureTask object, the state is set to NEW, and the callable value is assigned to the task we passed in.

The callable task will be executed in the run() method. Before execution, first determine that the task is in the NEW state and set the runner to the current thread through cas successfully. Then call call() to execute the task. After successful execution, the set() method will be called to assign the result to outcome. After the task execution throws an exception, the exception information will be called setException() and assigned to outcome. As for why the state needs to be changed to COMPLETING first and then to NORMAL, the main reason is to ensure that the outcome assignment has been completed in the NORMAL state. finishCompletion() will wake up (via LockSupport.unpark()) those threads (waiters) blocked by calling get().

 protected void set(V v) {
    
    
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
    
    
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

Calling the get() method will block the acquisition of results (or exceptions). If state > COMPLETING, indicating that the task has been completed (NORMAL, EXCEPTIONAL, CANCELLED, INTERRUPTED), the result will be returned directly through the report() method or an exception will be thrown. If state <= COMPLETING, it means that the task is still being executed or has not started yet, and the awaitDone() method is called to block and wait.

 public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
    
    
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }

The awaitDone() method uses state judgment to decide whether to return directly or add the current thread to waiters, and then calls the LockSupport.park() method to suspend the current thread.

There is also an important cancel() method, because the first sentence of the FutureTask source code class comment says that FutureTask is a cancelable asynchronous calculation. The code is also very simple. If the state is not NEW or the CAS assignment fails to INTERRUPTING / CANCELLED, it will return directly. On the other hand, if mayInterruptIfRunning = true, it means that the running thread may be interrupted, then the thread will be interrupted, the state will change to INTERRUPTED, and finally the waiting thread will be awakened.

    public boolean cancel(boolean mayInterruptIfRunning) {
    
    
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {
    
        // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
    
    
                try {
    
    
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally {
    
     // final state
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
    
    
            finishCompletion();
        }
        return true;
    }

The above briefly introduces the execution process of FutureTask. The space is limited and the source code is not interpreted very carefully. Later, you can consider publishing a separate article to analyze the source code of FutureTask.

What is a blocking queue? What are the blocking queues?

Blocking Queue BlockingQueue inherits Queue and is a special type of the basic data structure queue we are familiar with.

When retrieving data from a blocking queue, if the queue is empty, wait until an element is stored in the queue. When storing elements into the blocking queue, if the queue is full, wait until an element is removed from the queue. Provides common methods such as offer(), put(), take(), poll(), etc.

There are the following top 7 implementations of blocking queues provided by JDK:

1) ArrayBlockingQueue : A bounded blocking queue implemented by an array, which sorts elements according to FIFO. Maintain two integer variables to identify the positions of the head and tail of the queue in the array. The producer puts in data and the consumer obtains data by sharing a lock object, which means that the two cannot truly run in parallel and the performance is low;

2) LinkedBlockingQueue : A bounded blocking queue composed of a linked list. If the size is not specified, Integer.MAX_VALUE is used by default as the queue size. The queue sorts elements according to FIFO and maintains independent locks for producers and consumers. Data synchronization means that the queue has higher concurrency performance;

3) SynchronousQueue : A blocking queue that does not store elements, has no capacity, and can be set to fair or unfair mode. The insertion operation must wait for the acquisition operation to remove the element, and vice versa;

4) PriorityBlockingQueue : An unbounded blocking queue that supports priority sorting. By default, it is sorted according to natural order. Comparator can also be specified;

5) DelayQueue : An unbounded blocking queue that supports delayed acquisition of elements. When creating an element, you can specify how long it will take before the element can be obtained from the queue. It is often used in cache systems or scheduled task scheduling systems;

6) LinkedTransferQueue : An unbounded blocking queue composed of a linked list structure. Compared with LinkedBlockingQueue, it has more transfer and tryTranfer methods. This method will immediately pass the element to the consumer when there is a consumer waiting to receive the element;

7) LinkedBlockingDeque : A double-ended blocking queue composed of a linked list structure, which can insert and delete elements from both ends of the queue;

8) VariableLinkedBlockingQueue : After talking about the above blocking queues provided by JDK, we can also say that LinkedBlockingQueue is our most widely used blocking queue, but the capacity of LinkedBlockingQueue cannot be modified once it is defined. I have the need to dynamically adjust the capacity when using the thread pool, so I refer to the VariableLinkedBlockingQueue in RabbitMq and implement an enhanced version of the LinkedBlockingQueue that can adjust the capacity to achieve dynamic adjustment of the capacity;

9) MemorySafeLinkedBlockingQueue : And LinkedBlockingQueue uses Integer.MAX_VALUE as the capacity by default, which is an unbounded queue. There may be a risk of OOM, so I implemented a memory-safe MemorySafeLinkedBlockingQueue, which can configure the maximum remaining memory. When the memory reaches this When the value is set, placing the task in the queue will fail, which is a good guarantee that the troublesome OOM problem will not occur;

10) TaskQueue : The blocking queue was mentioned above when talking about the Tomcat thread pool. As a subclass of LinkedBlockingQueue, it overrides the offer(), poll(), take() and other methods to adjust the execution process of the thread pool.

Let’s focus on the two custom blocking queues 8 and 9 to highlight your rich experience in using blocking queues. The source codes of these two queues can be searched by yourself.
MemorySafeLinkedBlockingQueue & VariableLinkedBlockingQueue 队列实现

What are the thread pool rejection policies? What are the applicable scenarios?

When the blocking queue is full and the maximum number of threads is reached, resubmitting the task will go through the rejection strategy process. JDK provides the rejection strategy top-level interface RejectedExecutionHandler. All rejection strategies need to inherit this interface. JDK has four built-in rejection strategies.

1) AbortPolicy : The default rejection policy of the thread pool, which will throw a RejectedExecutionException exception when triggered. If it is some relatively important business, you can use this rejection policy to detect and handle problems in time by throwing exceptions when the system cannot further support larger concurrency;

2) CallerRunsPolicy : When the thread pool is not closed, the caller thread handles the task, otherwise it is discarded directly. This rejection strategy pursues tasks that can be executed without being lost. It is more suitable for scenarios where the amount of concurrency is not large and tasks are not allowed to be lost, and the performance is low;

3) DiscardPolicy : discards tasks, does not throw exceptions, and is generally imperceptible. It is recommended that this strategy be used for some insignificant tasks;

4) DiscardOldestPolicy : Discard the oldest task in the queue, and then resubmit the rejected task. You need to choose whether to use it based on the business scenario.

3. 4 Both of these rejection strategies will discard tasks without being aware of them. You need to decide whether to use them based on the business scenario.

You can also customize the rejection policy according to your own needs. For example, Dubbo defines the rejection policy AbortPolicyWithReport, which prints the thread stack information before throwing an exception.

What pitfalls have you encountered or what should you pay attention to when using the thread pool?

1) OOM problem :
When you first use threads, they are all created through Executors. As mentioned before, the thread pool created in this way will have the risk of OOM. You can give an example;

2) The problem of abnormal task execution loss :
it can be solved in the following 4 ways

Add try and catch exception handling in the task code;
if you use the Future method, you can receive the thrown exception through the get method of the Future object;
set setUncaughtExceptionHandler for the worker thread and handle the exception in the uncaughtException method;
you can override afterExecute(Runnable r, Throwable t) method, get exception t.

3) Shared thread pool problem :
The entire service shares a global thread pool, causing tasks to interact with each other. Long-consuming tasks occupy resources, and short-consuming tasks cannot be executed. At the same time, deadlock will occur between the parent and child threads, which will lead to OOM.

4) Used in conjunction with ThreadLocal, causing dirty data problems :
We know that Tomcat uses the thread pool to process received requests and reuses threads. If ThreadLocal is used in our code and does not remove after the request is processed, then each The request may obtain dirty values ​​left over from previous requests.

5) ThreadLocal will fail in the thread pool scenario. You can consider using Alibaba's open source TTL to solve it.

6) You need to customize the thread factory to specify the thread name, otherwise you won’t know how to locate the problem if it occurs.

Guess you like

Origin blog.csdn.net/weixin_43874301/article/details/132829983