[Thread] 7 ways to create thread pools and customize thread pools

1. What is a thread pool?

Thread Pool (ThreadPool) is a mechanism for managing and using threads based on the idea of ​​pooling: it stores multiple threads in a "pool" in advance, and when a task appears, it can avoid the performance caused by re-creating and destroying threads. Overhead, you only need to take out the corresponding thread from the "pool" to perform the corresponding task.

Using a thread pool has the following main advantages:

  1. Reduce resource consumption ( reuse threads and reduce the overhead caused by frequent thread creation and destruction )
  2. Improve response speed
  3. Improve thread manageability

at the same time,Alibaba also mandates in its "Java Development Manual" that thread resources must be provided through the thread pool, and explicit creation of threads in the application is not allowed.

2. Use thread pool

There are a total of 7 ways to create a thread pool, but generally they can be divided into 2 categories:

  • ThreadPoolExecutorThread pool created by
  • ExecutorsThread pool created by

There are a total of 7 ways to create a thread pool (6 of which are Executorscreated by and 1 is ThreadPoolExecutorcreated by ):

  1. Executors.newFixedThreadPool(): Create a fixed-size thread pool that can control the number of concurrent threads. Exceeding threads will wait in the queue;
  2. Executors.newCachedThreadPool(): Create a cacheable thread pool. If the number of threads exceeds the processing requirements, the excess threads will be recycled after a period of caching. If the number of threads is not enough, new threads will be created;
  3. Executors.newSingleThreadExecutor(): Create a thread pool with a single number of threads, which can guarantee the execution order of first-in, first-out;
  4. Executors.newScheduledThreadPool():Create a thread pool that can perform delayed tasks;
  5. Executors.newSingleThreadScheduledExecutor(): Create a single-threaded thread pool that can perform delayed tasks;
  6. Executors.newWorkStealingPool(): Create a thread pool for preemptive execution (the order of task execution is uncertain) JDK 1.8 added
  7. ThreadPoolExecutor: The most original way to create a thread pool, it contains 7 parameters to set

The significance of single-thread pool: Although newSingleThreadExecutorand newSingleThreadScheduledExecutorare single-thread pools, they provide functions such as work queues, life cycle management, and worker thread maintenance.

2.1 ThreadPoolExecutor

First, let’s look at the most original way to create a thread pool ThreadPoolExecutor:

public class ThreadPoolExecutorTest {
    
    

    public static void main(String[] args) {
    
    
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        // 执行任务
        for (int i = 0; i < 10; i++) {
    
    
            final int index = i;
            threadPool.execute(() -> {
    
    
                System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
                try {
    
    
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            });
        }
    }
}

ThreadPoolExecutor can set up to 7 parameters:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
     BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
    
    }

The meanings of the 7 parameters are as follows:

  1. corePoolSize : The number of core threads, the number of threads that are always alive in the thread pool.

  2. maximumPoolSize : The maximum number of threads, the maximum number of threads allowed in the thread pool, and the maximum number of threads that can be created when the task queue of the thread pool is full .

  3. keepAliveTime : The time that the maximum number of threads can survive. When no tasks are executed in the thread, part of the maximum thread will be destroyed, and finally the number of core threads will be maintained.

  4. unit : The unit is used in conjunction with parameter 3 survival time, and together they are used to set the survival time of the thread. The time unit of parameter keepAliveTime has the following 7 options:

    • TimeUnit.DAYS: days
    • TimeUnit.HOURS: Hours
    • TimeUnit.MINUTES: Minutes
    • TimeUnit.SECONDS: seconds
    • TimeUnit.MILLISECONDS: milliseconds
    • TimeUnit.MICROSECONDS: subtle
    • TimeUnit.NANOSECONDS: nanoseconds
  5. workQueue : A blocking queue used to store tasks waiting to be executed by the thread pool. They are all thread-safe. It is generally divided into direct submission queue, bounded task queue, unbounded task queue and priority task queue, including the following 7 types:

    • ArrayBlockingQueue: A bounded blocking queue composed of an array structure.
    • LinkedBlockingQueue: A bounded blocking queue composed of a linked list structure.
    • SynchronousQueue: A blocking queue that does not store elements, that is, submit them directly to the thread without retaining them.
    • PriorityBlockingQueue: An unbounded blocking queue that supports priority sorting.
    • DelayQueue: an unbounded blocking queue implemented using a priority queue, from which elements can be extracted only when the delay expires
    • LinkedTransferQueue: An unbounded blocking queue composed of a linked list structure. Similar to SynchronousQueue, it also contains non-blocking methods.
    • LinkedBlockingDeque: A two-way blocking queue composed of a linked list structure.

    The more commonly used ones are LinkedBlockingQueueand Synchronous, the queuing strategy of the thread pool is related to BlockingQueue

  6. threadFactory : Thread factory, mainly used to create threads.

  7. handler : rejection strategy, the strategy for rejecting processing tasks. The system provides 4 options:

    • AbortPolicy: Reject and throw an exception.
    • CallerRunsPolicy: Use the current calling thread to perform this task.
    • DiscardOldestPolicy: Discard a task at the head of the queue (oldest) and execute the current task.
    • DiscardPolicy: Ignore and discard the current task.

The default policy is AbortPolicy

Thread pool execution process

The execution flow of key nodes of ThreadPoolExecutor is as follows:

  1. Threads are created when the number of threads is less than the number of core threads.
  2. When the number of threads is greater than or equal to the number of core threads and the task queue is not full, the task is put into the task queue.
  3. When the number of threads is greater than or equal to the number of core threads and the task queue is full: if the number of threads is less than the maximum number of threads, create a thread; if the number of threads is equal to the maximum number of threads, an exception is thrown and the task is rejected.

Insert image description here

2.2 FixedThreadPool

FixedThreadPool: Create a fixed-size thread pool that can control the number of concurrent threads. Exceeding threads will wait in the queue.

public static ExecutorService newFixedThreadPool(int nThreads) {
    
    
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
  • corePoolSize is equal to maximumPoolSize , that is, all its threads are core threads, and it is a fixed-size thread pool, which is its advantage;
  • keepAliveTime = 0 This parameter is invalid for core threads by default, and FixedThreadPoolall are core threads;
  • The workQueue is LinkedBlockingQueue(unbounded blocking queue), and the maximum value of the queue is Integer.MAX_VALUE. If the task submission speed continues to be greater than the task processing speed, it will cause a large amount of queue congestion. Because the queue is large, it is very likely that the memory will overflow before the policy is rejected. is its disadvantage ;
  • FixedThreadPoolThe task execution isdisorderof;

Applicable scenarios: It can be used for instantaneous peak shaving of Web services, but attention must be paid to queue blocking caused by long-term peak conditions.

public class NewFixedThreadPoolTest {
    
    

    public static void main(String[] args) {
    
    
        System.out.println("主线程启动");
        // 1.创建1个有2个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(2);

        Runnable runnable = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    Thread.sleep(2000);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
            }
        };
        // 2.线程池执行任务(添加4个任务,每次执行2个任务,得执行两次)
        threadPool.submit(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        System.out.println("主线程结束");
    }
}

The above code: creates a thread pool with 2 threads, but assigns 4 tasks to it at a time, and can only execute 2 tasks at a time, so it must be executed twice.

This thread pool reuses a fixed number of threads running in a shared unbounded queue. At any time, up to nThreads threads will be active processing tasks. If other tasks are submitted while all threads are active, they will wait in the queue until a thread becomes available. So, it will execute 2 tasks (2 active threads) at a time, and the other 2 tasks are waiting in the work queue.

submit()Methods and execute()methods are ways to perform tasks. The difference between them is that submit()methods have return values, while execute()methods have no return values.

2.3 CachedThreadPool

CachedThreadPool: Create a cacheable thread pool. If the number of threads exceeds the processing requirements, the cache will be recycled after a period of time. If the number of threads is not enough, a new thread will be created.

public static ExecutorService newCachedThreadPool() {
    
    
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
  • corePoolSize = 0, maximumPoolSize = Integer.MAX_VALUE , that is, the number of threads is almost unlimited;
  • keepAliveTime = 60s , the thread will automatically end after being idle for 60s
  • workQueue is SynchronousQueuea synchronization queue. This queue is similar to a baton. Entry and exit must be passed at the same time. Because CachedThreadPoolthere is no limit on thread creation and there will be no queue waiting, so useSynchronousQueue

Applicable scenarios: Quickly process a large number of short-term tasks, such as NettyNIO, which can be used when accepting requests CachedThreadPool.

public class NewCachedThreadPool {
    
    

    public static void main(String[] args) {
    
    
        ExecutorService threadPool = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
    
    
            threadPool.execute(() -> {
    
    
                System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
                try {
    
    
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            });
        }
    }
}

Judging from the final result, the thread pool created 10 threads to perform the corresponding tasks

2.4 SingleThreadExecutor

SingleThreadExecutor: Create a thread pool with a single number of threads, which can guarantee the execution order of first-in, first-out.

public class SingleThreadExecutorTest {
    
    

    public static void main(String[] args) {
    
    
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
    
    
            final int index = i;
            threadPool.execute(() -> {
    
    
                System.out.println(index + ":任务被执行");
                try {
    
    
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            });
        }
    }
}

The execution results are as follows:
Insert image description here

2.5 ScheduledThread

ScheduledThreadPool: Create a thread pool that can perform delayed tasks.

public class ScheduledThreadTest {
    
    

    public static void main(String[] args) {
    
    
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
        System.out.println("添加任务,时间:" + new Date());
        threadPool.schedule(() -> {
    
    
            System.out.println("任务被执行,时间:" + new Date());
        }, 2, TimeUnit.SECONDS);
    }
}

The execution results are as follows:
Insert image description here
From the above results, we can see that the task was executed after 2 seconds, which is in line with our expectations.

2.6 SingleThreadScheduledExecutor

SingleThreadScheduledExecutor: Create a single-threaded thread pool that can perform delayed tasks.

public class SingleThreadScheduledExecutorTest {
    
    

    public static void main(String[] args) {
    
    
        ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
        System.out.println("添加任务,时间:" + new Date());
        threadPool.schedule(() -> {
    
    
            System.out.println("任务被执行,时间:" + new Date());
            try {
    
    
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
    
            }
        }, 2, TimeUnit.SECONDS);
    }
}

2.7 NewWorkStealingPool

NewWorkStealingPool: Create a thread pool for preemptive execution (the order of task execution is uncertain). Note that this method can only be used in JDK 1.8+ version

public class NewWorkStealingPoolTest {
    
    

    public static void main(String[] args) {
    
    
        ExecutorService threadPool = Executors.newWorkStealingPool();
        for (int i = 0; i < 10; i++) {
    
    
            final int index = i;
            threadPool.execute(() -> {
    
    
                System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
            });
        }
        // 确保任务执行完成
        while (!threadPool.isTerminated()) {
    
    
        }
    }
}

Judging from the above code, the execution order of tasks is uncertain because it is preemptively executed.

3. Task queue

Task queue: Generally divided into direct submission queue, bounded task queue, unbounded task queue, and priority task queue.

1. Submit the queue directly : Set it asSynchronousQueuea queue. SynchronousQueue is a special BlockingQueue. It has no capacity. Every time an insertion operation is performed, it will be blocked. It needs to perform another deletion operation before it can be awakened. On the contrary, every deletion operation will also need to be performed. Wait for the corresponding insert operation.

public class SynchronousQueueTest {
    
    

    public static void main(String[] args) {
    
    
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 100, TimeUnit.SECONDS, new SynchronousQueue<>());
        for (int i = 0; i < 3; i++) {
    
    
            threadPool.execute(() -> System.out.println(Thread.currentThread().getName()));
        }
    }
}

It can be seen that when the task queue is and SynchronousQueuethe number of threads created is greater than maximumPoolSize, the rejection policy is directly executed and an exception is thrown.

Using SynchronousQueuequeues, submitted tasks will not be saved and will always be submitted for execution immediately. If the number of threads used to perform tasks is less than maximumPoolSize, try to create a new process. If the maximum value set by maximumPoolSize is reached, the rejection policy is executed according to the handler you set. Therefore, the tasks you submit in this way will not be cached, but will be executed immediately. In this case, you need to have an accurate assessment of the concurrency of your program in order to set the appropriate maximumPoolSize amount, otherwise it will be very difficult. It is easy to implement a rejection policy

2. Bounded task queue : Bounded task queue can beArrayBlockingQueueimplemented

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

Using ArrayBlockingQueuea bounded task queue, if there are new tasks that need to be executed, the thread pool will create new threads. When the number of created threads reaches corePoolSize, the new tasks will be added to the waiting queue. If the waiting queue is full, that is, it exceeds the capacity initialized by ArrayBlockingQueue, threads will continue to be created until the number of threads reaches the maximum number of threads set by maximumPoolSize. If it is greater than maximumPoolSize, the rejection policy will be executed. In this case, the upper limit of the number of threads is directly related to the status of the bounded task queue. If the initial capacity of the bounded queue is large or has not reached an overloaded state, the number of threads will always be maintained below corePoolSize. Otherwise, when the task queue When it is full, maximumPoolSize will be used as the upper limit of the maximum number of threads.

3. Unbounded task queue : Bounded task queue can beLinkedBlockingQueueimplemented

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

Using an unbounded task queue, the thread pool's task queue can add new tasks without limit, and the maximum number of threads created by the thread pool is the number set by your corePoolSize, which means that in this case, the maximumPoolSize parameter is invalid, even if There are many unexecuted tasks cached in your task queue. When the number of threads in the thread pool reaches corePoolSize, it will not increase anymore. If new tasks are added later, they will directly enter the queue to wait. When using this kind of task queue mode, you must pay attention to the coordination and control between your task submission and processing, otherwise there will be a problem that the tasks in the queue will continue to grow because they cannot be processed in time, until the resources are finally exhausted.

4. Thread rejection strategy

Let's demonstrate ThreadPoolExecutorthe triggering of the rejection policy. We use DiscardPolicythe rejection policy. It will ignore and discard the policy of the current task. The implementation code is as follows:

public class ThreadPoolStrategyTest {
    
    

    public static void main(String[] args) {
    
    
        // 线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 100,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());
        // 任务
        Runnable runnable = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("当前任务被执行,执行时间:" + new Date() +
                        " 执行线程:" + Thread.currentThread().getName());

                try {
    
    
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        };
        // 开启4个任务
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
    }
}

We created a thread pool with the number of core threads and the maximum number of threads both equaling 1, and set the task queue of the thread pool to 1, so that when we have more than 2 tasks, the rejection policy will be triggered. The execution results are as follows Shown:
Insert image description here
From the above results, it can be seen that only two tasks were executed correctly, and other redundant tasks were discarded and ignored. The use of other rejection strategies is similar, so I won’t go into details here.

Custom rejection policy

In addition to the four rejection strategies provided by Java itself, we can also customize the rejection strategy. The sample code is as follows:

public class MyThreadPoolStrategyTest {
    
    

    public static void main(String[] args) {
    
    
        Runnable runnable = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("当前任务被执行,执行时间:" + new Date() +
                        " 执行线程:" + Thread.currentThread().getName());

                try {
    
    
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        };
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 100,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1), new RejectedExecutionHandler() {
    
    
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    
    
                // 执行自定义拒绝策略的相关操作
                System.out.println("我是自定义拒绝策略~");
            }
        });

        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
    }
}

The execution results of the program are as follows:

Insert image description here

Custom thread pool

The following is a demo of a custom thread pool using bounded queues, customization ThreadFactoryand rejection policies:

public class MyThreadPoolTest {
    
    

    public static void main(String[] args) throws Exception {
    
    
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
        NameThreadFactory threadFactory = new NameThreadFactory();
        RejectedExecutionHandler handler = new MyIgnorePolicy();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS,
                workQueue, threadFactory, handler);
        // 预启动所有核心线程
        executor.prestartAllCoreThreads();

        for (int i = 1; i <= 10; i++) {
    
    
            MyTask task = new MyTask(String.valueOf(i));
            executor.execute(task);
        }

        //阻塞主线程
        System.in.read();
    }

    static class NameThreadFactory implements ThreadFactory {
    
    
        private final AtomicInteger mThreadNum = new AtomicInteger(1);

        @Override
        public Thread newThread(Runnable r) {
    
    
            Thread t = new Thread(r, "my-thread-" + mThreadNum.getAndIncrement());
            System.out.println(t.getName() + " has been created");
            return t;
        }
    }

    static class MyIgnorePolicy implements RejectedExecutionHandler {
    
    
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    
    
            doLog(r, executor);
        }

        private void doLog(Runnable r, ThreadPoolExecutor e) {
    
    
            // 可做日志记录等
            System.err.println( r.toString() + " rejected");
            System.out.println("completedTaskCount: " + e.getCompletedTaskCount());
        }
    }

    static class MyTask implements Runnable {
    
    
        private String name;

        public MyTask(String name) {
    
    
            this.name = name;
        }

        @Override
        public void run() {
    
    
            try {
    
    
                System.out.println(this.toString() + " is running!");
                // 让任务执行慢点
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

        public String getName() {
    
    
            return name;
        }

        @Override
        public String toString() {
    
    
            return "MyTask [name=" + name + "]";
        }
    }
}

Insert image description here
Among them, threads 1-4 first occupied the core threads and the maximum number of threads, and then tasks 4, 5, and 6 entered the waiting queue. Task 7-10, task 5, was directly ignored and refused to be executed. 4 was notified after threads in tasks 1-4 were executed. ,5,6 tasks continue to be executed.

5. Thread pool tool class

public class TaskCenterUtil {
    
    

    public static Integer CORE_POOL_SIZE = 10;
    public static Integer MAX_NUM_POOL_SIZE = 10;
    public static Integer MAX_MESSAGE_SIZE = 100;
    public static Long KEEP_ALIVE_TIME = 60L;

    private final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_NUM_POOL_SIZE, KEEP_ALIVE_TIME,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(MAX_MESSAGE_SIZE), new CustomerThreadFactory("zzc-thread-pool"), new ThreadPoolExecutor.CallerRunsPolicy());

    static class CustomerThreadFactory implements ThreadFactory {
    
    
        private String name;
        private final AtomicInteger mThreadNum = new AtomicInteger(1);
        public CustomerThreadFactory(String name) {
    
    
            this.name = name;
        }
        @Override
        public Thread newThread(Runnable r) {
    
    
            return new Thread(r, name + + mThreadNum.getAndIncrement());
        }
    }

    private TaskCenterUtil() {
    
    }

    private static final TaskCenterUtil taskCenterUtil = new TaskCenterUtil();

    public static TaskCenterUtil getTaskCenterUtil() {
    
    
        return taskCenterUtil;
    }

    // 提交任务(有返回值)
    public <T> Future<T> submitTask(Callable<T> task) {
    
    
        return poolExecutor.submit(task);
    }

    public void submitTask2(Callable task) {
    
    
        poolExecutor.submit(task);
    }

    // 提交任务(无返回值)
    public void executeTask(Runnable task) {
    
    
        poolExecutor.execute(task);
    }

    public void shutdown() {
    
    
        poolExecutor.shutdown();
    }
}

use:

1. Use lambda

TaskCenterUtil.getTaskCenterUtil().submitTask2(() -> {
    
    
	log.info("【批量添加】批量添加数据:{}", JSON.toJSONString(excelVos));
    return null;
});

2.Use classes

public class DeviceDataTask implements Runnable {
    
    
	@Override
    public void run() {
    
    
        log.info("【批量添加】批量添加数据:{}", JSON.toJSONString(excelVos));
    }
}
TaskCenterUtil taskCenterUtil = TaskCenterUtil.getTaskCenterUtil();
taskCenterUtil.executeTask(new DeviceDataTask());
taskCenterUtil.shutdown();

6. Which thread pool to choose?

After the above study, we have a certain understanding of the entire thread pool, so how to choose a thread pool?

Let’s take a look at the answer given to us by Alibaba’s “Java Development Manual”:

[Mandatory] Thread pools are not allowed to be created using Executors, but through ThreadPoolExecutor. This processing method allows students who write to know more clearly the operating rules of the thread pool and avoid the risk of resource exhaustion.

Note: The disadvantages of the thread pool objects returned by Executors are as follows:
1) FixedThreadPool and SingleThreadPool: The allowed request queue length is Integer.MAX_VALUE, which may accumulate a large number of requests, resulting in OOM.
2) CachedThreadPool: The allowed number of threads to create is Integer.MAX_VALUE, which may create a large number of threads, causing OOM.

Therefore, to sum up the above situation, we recommend using ThreadPoolExecutorthe method to create a thread pool, because this method of creation is more controllable and clarifies the operating rules of the thread pool, which can avoid some unknown risks.

Guess you like

Origin blog.csdn.net/sco5282/article/details/120963463