郑州自助建站软件,郑州app开发公司哪家比较好,北京住总第三开发建设有限公司网站,国外平面设计欣赏网站一文搞懂ES中的线程池 - 知乎
ES线程池设置-阿里云开发者社区
文章目录 一、简介 二、线程池类型 2.1、fixed 2.2、scaling 2.3、direct 2.4、fixed_auto_queue_size 三、处理器设置 四、查看线程池 4.1、cat thread pool 4.2、nodes info 4.3、nodes stats 4.4、no…一文搞懂ES中的线程池 - 知乎
ES线程池设置-阿里云开发者社区
文章目录 一、简介 二、线程池类型 2.1、fixed 2.2、scaling 2.3、direct 2.4、fixed_auto_queue_size 三、处理器设置 四、查看线程池 4.1、cat thread pool 4.2、nodes info 4.3、nodes stats 4.4、nodes hot threads 4.5、Java 的线程池结构 五、ES的线程池实现 5.1、ThreadPool 类结构与初始化 5.2、fixed类型线程池构建过程 5.3、scaling类型线程池构建过程 5.4、direct类型线程池构建过程 5.5、fixed_auto_queue_size类型线程池构建过程 六、其他线程池 七、思考与总结
一、简介
每个节点都会创建一系列的线程池来执行任务许多线程池都有与其相关任务队列用来允许挂起请求而不是丢弃它。 下面列出目前ES版本中的线程池。
官网 https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-threadpool.html generic 用于通用的操作(例如节点发现)线程池类型为 scaling。 index 用于index/delete操作线程池类型为fixed大小为处理器的数量队列大小为200允许设置的最大线程数为1处理器数量。 search 用于count/search/suggest操作。线程池类型为fixed, 大小为int((处理器数量3)/2)1队列大小为1000。 get 用于get操作。线程池类型为fixed, 大小为处理器的数量队列大小为1000。 bulk 用于bulk操作线程池类型为fixed,大小为处理器的数量队列大小为200该线程池允许设置的最大线程数为1处理器数量。 snapshot 用于snaphostrestore操作。线程池类型为scaling,线程保持存活时间为5min,最大线程数为min(5, (处理器数量)/2)。 warme 用于segment warm-up操作。线程池类型为scaling 线程保持存活时间为5min最大线程数为min(5, (处理器数量)/2)。 refresh 用于refresh 操作。线程池类型为scaling, 线程空闲保持存活时间为5min最大线程数为min(10, (处理器数量)/2)。 listener 主要用于Java客户端线程监听器被设置为true时执行动作。线程池类型为scaling,最大线程数为min(10, (处理器数量)/2)。 same 在调用者线程执行不转移到新的线程池。 management 管理工作的线程池例如Node info、Node tats、 List tasks等。 flush 用于索引数据的flush操作。 force_merge 顾名思义用于Lucene分段的force merge。 fetch_shard_started 用于TransportNodesAction. fetch_shard_store 用于TransportNodesListShardStoreMetaData。 thread_pool.search.size: 30 线程池和队列的大小可以通过配置文件进行调整,例如为search增加线程数和队列大小:
二、线程池类型
如同任何要并发处理任务的服务程序一样线程池要处理的任务类型大致可以分为两类:CPU计算密集型和I/O密集型。对于两种不同的任务类型,需要为线程池设置不同的线程数量。
一般说来线程池的大小可以参考如下设置其中N为CPU的个数: 对于CPU密集型任务线程池大小设置为N1; 对于I/O密集型任务线程池大小设置为2N1
对于计算密集型任务线程池的线程数量 一 般不应该超过N1。如果线程数量太多则会导致更高的线程间上下文切换的代价。加1是为了当计算线程出现偶尔的故障,或者偶尔的I/O、发送数据、写日志等情况时这个额外的线程可以保证CPU时钟周期不被浪费。
I/O密集型任务的线程数可以稍大一些因为I/O密集型任务大部分时间阻塞在I/O过程适当增加线程数可以增加并发处理能力。而上下文切换的代价相对来说已经不那么敏感。但是线程数量不一定设置为2N1,具体需要看I/O等待时间有多长。等待时间越长需要越多的线程等待时间越少需要越少的线程。因此也可以参考下面的公式:
最佳线程数 ((线程等待时间 线程CPU时间) 线程CPU时间) * CPU数为了应对这两种类型的任务ES封装了以下类型的线程池。
2.1、fixed
线程池拥有固定数量的线程来处理请求当线程空闲时不会销毁当所有线程都繁忙时请求被添加到队列中。 size 参数用来控制线程的数量。 queue_size 参数用来控制线程池相关的任务队列大小。设置为-1表示无限制。当请求到达时如果队列已满则请求将被拒绝。
例如:
thread_pool.search.size: 30
thread_pool.search.queue_size: 15002.2、scaling
scaling 线程池的线程数量是动态的介于core和max参数之间变化。线程池的最小线程数为配置的core大小随着请求的增加当core数量的线程全都繁忙时线程数逐渐增大到max数量。max是线程池可拥有的线程数.上限。当线程空闲时线程数从max大小逐渐降低到core大小。 keep_alive 参数用来控制线程在线程池中的最长空闲时间。
例如
thread_pool.warmer.core: 1
thread_pool.warmer.max:8
thread_pool.warmer.keep_alive: 2m 2.3、direct
这种线程池对用户并不可见,当某个任务不需要在独立的线程执行又想被线程池管理时于是诞生了这种特殊类型的线程池:在调用者线程中执行任务。
2.4、fixed_auto_queue_size
与fixed类型的线程池相似该线程池的线程数量为固定值但是队列类型不一样。 其队列大小根据利特尔法则( Littles Law) 自动调整大小。该法则的详细信息可以参考https://en.wikipedia.org/wiki/Little%27s_law。 该线程池有以下参数可以调整: size 用于指定线程数量; queue_size ,指定初始队列大小; . min_queue_size 最小队列大小; max_queue_size 最大队列大小; auto_gueue_frame_size 调整队列之前进行测量的操作数; target_response_time 一个时间值设置用来指示任务的平均响应时间如果任务经常高于这个时间则线程池队列将被调小以便拒绝任务。
该线程类型为实验性质未来可能会移除。目前只有search线程池使用这种类型。 Deprecated in 7.7.0 and will be removed in 8.0.
三、处理器设置
https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-threadpool.html#node.processors
默认情况下ES自动探测处理器数量。各个线程池的大小基于这个数量进行初始化。在某些情况下如果想手工指定处理器数量则可以通过设置 processors 参数实现
processors: 2有以下几种场景是需要明确设置processors数量的
(1)、在同一台主机上运行多个ES实例但希望每个实例的线程池只根据一部分CPU来设置此时可以通过processors参数来设置处理器数量。例如在16核的服务器上运行2个实例可以将processors设置为8。请注意在单台主机上运行多个实例除了设置processors数量还有许多更复杂的参数需要设置。例如修改GC线程数绑定进程到CPU等。
(2)、有时候自动探测出的处理器数量是错误的在这种情况下需要明确设置处理器数量。要检查自动探测的处理器数量可以使用节点信息API中的os字段来查看。
四、查看线程池
ES提供了丰富的API查看线程池状态在监控节点健康、排查问题时非常有用。
4.1、cat thread pool
该命令显示每个节点的线程池统计信息。默认情况下所有线程池都返回活动、队列和被拒绝的统计信息。我们需要特别留意被拒接的信息例如bulk 请求被拒绝意味着客户端写入失败。在正常情况下客户端应该捕获这种错误(错误码429)并延迟重试但有时客户端不一定对这种错误做了处理导致写入集群的数据量低于预期值。
curl -X GET localhost:9200/_cat/thread_pool返回信息如下 在这里插入图片描述 active 表示当前正在执行任务的线程数量 queue 表示队列中等待处理的请求数量 rejected 表示由于队列已满请求被拒绝的次数。
对返回结果进行过滤等更多用法可参考官方手册: https://www.elastic.co/guide/en/elasticsearch/reference/6.1/cat-thread-pool.html。
4.2、nodes info
节点信息API可以返回每个线程池的类型和配置信息例如线程数量、队列大小等。下面的第一条命令返回所有节点的信息第二条命令返回特定节点的信息。.
curl -X GET localhost:9200/_nodes
curl -X GET localhost:9200/_nodes/nodeId1, nodeId2节点信息API返回的信息非常大其中与线程池相关信息在thread_ pool 字段中选取部分信息如下:
thread_pool : {force_merge : {type : fixed,min : 1,max : 1,queue_size : - 1},fetch_shard started : {type : scaling,min : 1,max : 16,keep_alive : 5m,queue_size : -1}
}该命令的完整信息可参考官方手册: https://www.elastic.co/guide/en/elasticsearch/reference/6.1/cluster-nodes-info.html
4.3、nodes stats
statsAPI返回集群中一个或全部节点的统计数据。
下面的第一条命令返回所有节点的统计数据第二条命令返回特定节点的统计数据。
curl -X GET localhost:9200/_nodes/stats curl -X GET localhost:9200/_nodes/nodeId1, nodeId2/stats
默认情况下该API返回全部 indices、oS、process、jvm、transport、http、fs、breaker 和thread_pool 方面的统计数据。其中线程池相关的返回结果摘要如下:
thread_pool : {bulk : {threads : 0,queue : 0,active : 0,rejected : 0largest : 0completed : 0}
}该命令的完整使用方式可参考官方手册: https://www.elastic.co/guide/en/elasticsearch/reference/6.1/cluster-nodes-stats.html。
4.4、nodes hot threads
该API返回集群中一个或全部节点的热点线程。
当发现节点进程占用CPU非常高时想知道是哪些线程导致的这些线程具体在执行什么操作常规做法是通过top命令配合jstack来定位线程现在ES提供了更便捷的方式通过hot threads API可以直接返回这些信息。
下面的第一条命令返回所有节点的热点线程第二条命令返回特定节点的热点线程。
curl -X GET localhost:9200/_nodes/hot_threads
curl -X GET localhost:9200/nodes/nodeId1, nodeId2/hot_threads该命令支持以下参数 threads 返回的热点线程数默认为3。 intervalES 对线程做两次检查,来计算某个操作.上花费时间的百分比此参数定义这个间隔时间。默认为500 ms。 type 定义要检查的线程状态类型,默认为CPU.API可以检查线程的CPU占用时间、阻塞(block)时间和等待(wait) 时间。 ignore_idle_threads 如果设置为true 则空闲线程(例如在套接字中等待或者从空队列中获取任务)将被过滤。默认值为true。
其返回信息的样例如下图所示。 在这里插入图片描述
返回信息中的第一行表明这个是哪个节点的信息以及这个节点的IP地址等。
::: {node1} {un- 9UZ4PS8-K6hF59x1MWA} {bjk2C_ _6USh0YgMYKcBWKLQ} {node1.eshost}
{10.10.13.15:9300}接下来列出哪个线程占用较多的CPU以及CPU的占用比
82.2% (411.2ms out of 500ms) cpu usage by thread elasticsearch [node1]
[bulk] [T#1] 最后是该线程的堆栈信息。
ES中的线程池是基于对Java线程池的封装和扩展。我们先看一下Java线程池的结构和使用方式这些是ES内部线程原理的基础知识。
4.5、Java 的线程池结构
Java内部的线程池称为Executor框架几个基本的类概念如下 Runable定义一个要执行的任务。 Executor提供了execute 方法接受一个Runable实例用来执行一个任务。 ExecutorService是线程池的虚基类继承自Executor, 提供了shutdown, shutdownNow等关闭线程池接口。 ThreadPoolExecutor线程池的具体实现。继承自ExecutorService维护线程创建、线程生命周期、任务队列等。 EsThreadPoolExecutor是ES对ThreadPoolExecutor的扩展实现。未来会增加一些统计信息。
这几个类的继承结构如下图所示。 我们以一个简单的例子来看看Java 线程池的用法ExecutorService 类用于保存创建的线程池实例后续调用execute方法执行任务。在下面的例子中任务类TestRunnable只是打印当前线程名称。
import java. util.concurrent.ExecutorService;
import java. util.concurrent.Executors;
publicclass ThreadPoolDemo {public static void main(String[] args) {//通过Executors构建一个固定大小的线程池线程数量为2返回线程池实例ExecutorService executorService Executors.newFixedThreadPool (2) ;//调用 线程池的execute方法执行一个任务executorService.execute(new TestRunnable());}
}
class TestRunnable implements Runnable {public void run() {System.out.println (Thread.currentThread().getName()) ;}
}ES内部创建线程池时返回类型同样是ExecutorService类。接下来我们通过构建过程来看看ThreadPoolExecutor的结构其构造函数如下:
public ThreadPoolExecutor (int corePoolSize,int maximumPoolSize,long keepAl iveTime,TimeUnit unit,BlockingQueueRunnable workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize 0 || max.imumPoolSize 0 || maximumPoolSize corePoolSize ||keepAliveTime 0)thrownew IllegalArgumentException() ;if (workQueue null || threadFactory null || handler null)thrownewNul lPointerException() ;this.corePoolSize corePoolSize;this.maximumPoolSize maximumPoolSize;this.workQueue workQueue ;this.keepAliveTime unit.toNanos (keepAliveTime) ;this.threadFactory threadFactory;this.handler handler;
}几个重要参数的含义如下 corePoolSize 线程池大小; maximumPoolSize 最大线程数量; keepAliveTime 线程空闲回收时间; BlockingQueue 任务队列; handler 队列满拒绝请求时的回调函数。
ThreadPoolExecutor类是Java线程池的具体实现是整个线程池中最重要的类ES 基于这个类进行了一些扩展。
五、ES的线程池实现
**ES中使用的线程池绝大部分封装在ThreadPool类中,**个别独立线程池的实现在本章末尾讨论。除了个别情况在ThreadPool类中会创建各个模块要使用的全部线程池。本章开始所讨论的几种线程池就是在ThreadPool类中创建的。
ThreadPool类创建各个线程池要使用线程池的各个内部模块会引用ThreadPool类对象通过其对外提供executor方法根据线程池名称获取对应的线程池引用进而执行某个任务。
ThreadPool对外提供的重要方法如下表所示。 在这里插入图片描述
当某个模块要在新的线程中启动任务时典型的使用方式如下:
threadPool.executor(ThreadPool.Names.SNAPSHOT).execute(() -beginSnapshot (newState, newSnapshot, request.partial(), listener));threadPool.executor方法返回snapshot线程池( ExecutorService类)的引用通过线程池的execute方法执行任务在本例中任务的Runnable是Lambda表达式定义的。5.1、ThreadPool 类结构与初始化
ThreadPool类对象在节点启动时初始化在Node类的构造函数中初始化ThreadPool类:
final ThreadPool threadPool new ThreadPool (settings, executorBuilders.toArray (new ExecutorBuilder[0]));线程池对象构建完毕将这个类的引用传递给其他要使用线程池的模块:
final ResourcewatcherService resourceWatcherService new ResourceWatcherService (settings, threadPool);线程池的名称在内部类Names中最好记住它们的名字有时需要通过jstack查看堆栈ES的堆栈非常长这就需要通过线程池的名称去查找关注的内容。
publicstaticclass Names {publicstaticfinal String SAME same;publicstaticfinal String GENERIC generic;publicstaticfinal String LISTENER listener;publicstaticfinal String GET get;publicstaticfinal String INDEX index ;publicstaticfinal String BULK bulk;publicstaticfinal String SEARCH search;publicstaticfinal String MANAGEMENT management ;publicstaticfinal String FLUSH flush;publicstaticfinal String REFRESH refresh;publicstaticfinal String WARMER warmer ;publicstaticfinal String SNAPSHOT snapshot;publicstaticfinal String FORCE MERGE force_merge;publicstaticfinal String FETCH\_SHARD\_STARTED fetch\_shard\_started;publicstaticfinal String FETCH\_SHARD\_STORE fetch\_shard\_store;
}线程池类型由枚举类型ThreadPoolType 定义
enum ThreadPoolType {DIRECT (direct) ,FIXED (fixed) ,FIXED_AUTO_QUEUE_SIZE(fixed_auto_queue_size) ,SCALING (scaling) ;
}在ThreadPool类构造函数中全部的线程池被初始化
public ThreadPool (final Settings settings, final ExecutorBuilder? ... customBuilders) {final MapString, ExecutorBuilder builders new HashMap () ;builders.put(Names.GENERIC, new ScalingExecutorBuilder(Names.GENERIC, 4, genericThreadPoolMax, TimeValue. timeValueSeconds (30))) ;builders.put (Names. INDEX, new FixedExecutorBuilder (settings, Names.INDEX, availableProcessors, 200)) ;//index/delete操作与bulk使用同一个线程池builders.put(Names.BULK, new FixedExecutorBuilder(settings, Names.BULK, availableProcessors, 200));builders.put(Names.GET, new FixedExecutorBuilder(settings, Names.GET, availableProcessors, 1000)) ;builders.put (Names.SEARCH, new AutoQueueAdjustingExecutorBuilder(settings, Names.SEARCH, searchThreadPoolSiz (availableProcessors), 1000, 1000, 1000, 2000));builders.put (Names.MANAGEMENT, newScal ingExecutorBuilder(Names.MANAGEMENT, 1, 5, TimeValue.timeValueMinutes(5))) ;builders.put (Names.LISTENER, new FixedExecutorBuilder (settings, Names.LISTENER, halfProcMaxAt10, -1));builders.put (Names.FLUSH, new ScalingExecutorBuilder (Names.FLUSH, 1, halfProcMaxAt5, TimeValue.timeValueMinutes(5))) ;builders.put (Names . REFRESH, new ScalingExecutorBuilder(Names.REFRESH, 1, halfProcMaxAt10, TimeValue.timeValueMinutes(5))) ;builders.put (Names.WARMER, newScal ingExecutorBuilder (Names.WARMER, 1, halfProcMaxAt5, TimeValue.timeValueMinutes(5))) ;builders.put (Names.SNAPSHOT, new ScalingExecutorBuilder(Names.SNAPSHOT, 1, halfProcMaxAt5, TimeValue.timeValueMinutes(5))) ;builders.put (Names.FETCH_SHARD_STARTED, new ScalingExecutorBuilder(Names.FETCH_SHARD_STARTED, 1, 2 * availableProcessors, TimeValue.timeValueMinutes(5))) ;builders.put (Names.FORCE_MERGE, new FixedExecutorBuilder (settings, Names.FORCE_MERGE, 1, -1) ) ;builders.put (Names.FETCH_SHARD_STORE, newScal ingExecutorBuilder(Names.FETCH_SHARD_STORE, 1, 2 * availableProcessors, TimeValue.timeValueMinutes(5))) ;
}这些线程池构建成功后最终保存到一个map结构中map 列表根据builders信息构建 将SAME线程池单独添加进去。
MapString, ExecutorHolder executors当某个模块使用线程池时通过线程池名称从这个map中获取对应的线程池。
public ExecutorService executor (String name) {final ExecutorHolder holder executors.get(name);return holder.executor() ;
}map 中的值 ExecutorHolder 是 ThreadPool 内部类它只是简单封装了一下ExecutorService。
class ExecutorHolder {privatefinal ExecutorService executor ;//Info类主要是线程池名称、类型、队列大小、线程数量的max和min、keepAlive时间publicfinal Info info;
}5.2、fixed类型线程池构建过程
FіхеdЕхесutоrВuіldеr 类用于fixed类型的线程池构建它的主要实现是通过 ЕѕЕхесutоrѕ.newFixed 方法构建一个ExecutorService。由于是fixed类型的线程池因此EsThreadPoolExecutor传入的corePoolSize 和 maximumPoolSize 的大小相同。
public static EsThreadPoolExecutor newFixed (String name, int size, int queueCapacity, ThreadFactory threadFactory, ThreadContext contextHolder) {//使用有限或无限大小的阻塞队列初始化线程池队列BlockingQueueRunnable queue;if (queueCapacity 0) fqueue Concur rentCollections . newBlockingQueue () ;} else {queue new SizeBlockingQueue (ConcurrentCollections.Runnable newBlockingQueue(), queueCapacity) ;}//创建线程池returnnew EsThreadPoolExecutor (name, size, size, 0, TimeUnit.MILLISECONDS, queue, threadFactory, new EsAbortPolicy(), contextHolder) ;
}5.3、scaling类型线程池构建过程
ScalingExecutorBuilder用于 scaling类型线程池的构建它的主要实现是通过EsExecutors.newScaling方法创建一个ExecutorService, min 和 max 分别对应corePoolSize 和 maximumPoolSize。
public static EsThreadPoolExecutor newScaling (String name, int min, int max, long keepAliveTime, TimeUnit unit, ThreadFactory threadFactory, ThreadContext contextHolder) {//创建线程队列ExecutorScal ingQueueRunnable queue new ExecutorScalingQueue() ;//min corePoolSize, max maximumPoolSizeEsThreadPoolExecutor executor new EsThreadPoolExecutor (name, min, max, keepAliveTime, unit, queue, threadFactory, new ForceQueuePolicy(), contextHolder) ;queue.executor executor;return executor ;
}5.4、direct类型线程池构建过程
direct类型的线程池没有通过*ExecutorBuilder类创建而是通过EsExecutors.newDirectExecutorService 方法直接创建的该方法中直接返回一个定义好的简单的线程池DIRECT_EXECUTOR_SERVICE 。该线程池的实现如下在execute方法中直接运行这个任务因此任务在调用者所执行。
privatestaticfinal ExecutorService DIRECT\_EXECUTOR\_SERVICE new AbstractExecutorService() {//不支持关闭public void shutdown() {thrownew UnsupportedOperationException() ;}//不支持中止public boolean awaitTermination (long timeoutTimeUnit unit) throws InterruptedException {thrownewUnsuppor tedOperationException() ;}//直接调用任务的运行方法因此在调用者的线程中执行任务public void execute (Runnable command) {command.run () ;}
};5.5、fixed_auto_queue_size类型线程池构建过程
该类型的线程池通过AutoQueueAdjustingExecutorBuilder 类构建构建过程的主要实现是通过EsExecutors.newAutoQueueFixed 方法创建一个ExecutorService 。 该线程池的队列是一个大小可调整的队列而QueueResizingEsThreadPoolExecutor 继承自EsThreadPoolExecutor , 在它的基础上实现了动态调整队列大小的利特尔法则( Littles Law)。
public static EsThreadPoolExecutor newAutoQueueFixed(String name, int size, int initialQueueCapacity, int minQueueSize, int maxQueueSize, int frameSize, TimeValue targetedResponseTime, ThreadFactory threadFactory, ThreadContext contextHolder) {//初始化大小必须大于1if (initialQueueCapacity 0) {thrownew IllegalArgumentException() ;}//初始化一个动态调整的队列ResizableBlockingQueueRunnable queue new ResizableBlockingQueue (ConcurrentCollections. RunnablenewBlockingQueue() , initialQueueCapacity) ;//创建线程池returnnewQueueResi Z ingEsThreadPool Executor (name, size, size, 0, TimeUnit.MILLISECONDS, queue, minQueueSize, maxQueueSize, TimedRunnable::new, frameSize, targetedResponseTime, threadFactory, new EsAbortPolicy(), contextHolder) ;
}六、其他线程池
除了ThreadPool中封装的各种线程池ES中还有一种支持优先级的线程池: PrioritizedEs-ThreadPoolExecutor 这个线程池同样继承自EsThreadPoolExecutor 类。目前只有主节点执行集群任务以及从节点应用集群状态时使用该类型的线程池。
该线程池通过EsExecutors.newSinglePrioritizing方法构建线程池有固定大小线程数为1。
public static PrioritizedEsThreadPoolExecutor newSinglePrioritizing (String name, ThreadFactory threadFactory, ThreadContext contextHolder, ScheduledExecutorService timer) {//corePoolSize和maximumPoolSize都为1returnnew PrioritizedEsThreadPoolExecutor (name, 110L, TimeUnit.MILLISECONDSthreadFactory, contextHolder, timer) ;
}在PrioritizedEsThreadPoolExecutor类的构造函数中,会为线程池创建一个支持优先级的队列: PriorityBlockingQueue,该队列是JDK中的实现,初始大小为11,最大为Integer.MAX_VALUE-8,基本是无限的。关于该队列的更多信息可以参考JDK手册: htps://ocs.oracle.com/javase/10/docs/api/java/util/concurrentPriorityBlockingQueue.html。
七、思考与总结 (1) 每种不同类型的线程池有各自不同的队列类型。scaling类型的线程池动态维护线程池数量。fixed_auto_queue_size 与fix类型的线程池都有固定的线程数。 (2) ThreadPool 类在节点启动时初始化然后将类的引用传递给其他模块其他模块通过明确指定线程名称从ThreadPool类中获取对应的线程池然后执行自定义的任务。 (3)节点关闭时对线程池模块先调用shutdown,等待10秒后执行shutdownNow。因此线程池中的任务有机会执行完毕但在超时后会尝试终止线程池中的任务。