每日一学:线程池的工作方式,线程池的参数配置,线程池的关闭,线程池的监控

tech2022-09-18  112

线程池的工作方式

创建方式,使用 new ThreadPoolExecutor 来创建,而不是使用 Executors 等工具类来创建,主要参数,核心线程数 corePoolSize,最大线程数 maxPoolSize,保活时间 keepAliveTime,阻塞队列 blockingQueue,线程工厂 threadFactory,拒绝策略 rejectHandler; // 不要使用 newFixedThreadPool :固定线程数,但任务队列是无界的,可能会撑爆内存 newCachedThreadPool :没有存储空间的队列,最大线程 Integer.MAX_VALUE // 构造初始化 new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, BlockingQueue<Runnable> runnableTaskQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler); 阻塞队列: ArrayBlockingQueue:基于数组实现的有界队列 LinkedBlockingQueue:基于链表的无界队列 SynchronousQueue:不存储元素的阻塞队列,每次插入操作必须等到另一个线程调用移除操作,等同于无存储空间的队列 PriorityBlockingQueue:具有优先级的无界队列,适合任务有优先级的场景 线程工厂 threadFactory: 创建线程的工厂类,通常用来自定义线程工厂,给每个新创建线程,设置有意义的名称,方便进行 debug 和定位问题 rejectedExecutionHandler,拒绝策略,如果队列满了,线程数也达到最大线程数,此时再提交的任务,就会采用拒绝策略; 常用的拒绝策略: AbortPolicy:丢弃任务并抛出异常 DiscardPolicy:丢弃掉当前提交的任务 DiscardOldestPolicy:丢弃队列中最近的一个任务,并执行当前任务 CallerRunsPolicy:使用调用者所在线程来执行任务 自定义拒绝策略,实现 RejectedExecutionHandler 接口

工作方式,默认 ThreadPoolExecutor,不初始化核心线程,每当一个任务到来,才创建一个核心线程执行;当核心线程数占满了后不会立即扩容,新任务会先堆积到阻塞队列;当阻塞队列占满了后才会扩容线程,直到到达最大线程数;最后线程数达到最大线程数,且队列占满,新任务会执行拒绝策略,keepAliveTime 是指大于核心线程数的线程在等待一定时间后会被销毁;

改变工作方式,声明线程池后立即调用 prestartAllCoreThreads(),可以来启动所有核心线程,调用 allowCoreThreadTimeOut(true) ,可以让线程池在空闲时同样回收核心线程;

execute(Runnable run),无返回值,无法得知任务执行结果;submit(Runnable run),返回一个 Future 类,通过其 get 方法阻塞获取任务执行结果,并且 get 可以指定一个阻塞时间,避免长时间阻塞等待;

ThreadPoolExecutor 的一个问题是,扩容线程往往不够及时,在阻塞队列占满后才会扩容线程,为此可以做一个改变,重写阻塞队列,让其最开始就返回队列满的假象,使得先扩容线程,同时要改写拒绝策略,让无法调度的任务插入到队列中,tomcat 线程池就实现了类似的效果,先扩容线程,后加入阻塞队列;

线程池的参数配置

线程数的计算方式,接口 TPS 100,接口响应时间 100ms,表示 1s 内会有 100 个请求,每个请求耗时 0.1 s,串行需要 10s,因此需要 100*0.1 = 10 个线程,才能保证接口 TPS 100,通常要考虑的一个问题是,当接口响应时间变慢时,线程数和 TPS 的变化;

对于 IO 绑定任务,执行时间长,cpu 利用率不高,要优先创建更多线程数,而不是建立大的阻塞队列;对于 cpu 绑定任务,线程数可定义为 cpu 核数,或 cpu 核数 * 2,因为任务占 cpu 时间,过多线程反而需要 cpu 频繁切换上下文,开销大;

// 获得硬件的 cpu 个数 Runtime.getRuntime().availableProcessors()

线程池不要盲目复用,不同类型任务,执行时间长短不一的任务需要单独创建线程池,但也不能每次都创建新的线程池,注意 Java 8 的 parallel stream,背后是共享同一个 ForkJoinPool;

一般都需要使用有界队列和可控的线程数,队列可根据需求设置大一点,但要考虑队列过大,一直不会满,最大线程数就没意义了,或者最后才扩容线程,滞后了;

线程池关闭

通常使用 shutdown 来关闭线程池,如果正在执行的任务,不需要执行完,可以调用 shutdownNow,正常的 ThreadPoolExecutor 并不会被回收,因为核心线程会一直存在,不会销毁;

shutdown(),会将线程池的状态置为 shutdown,中断所有没任务调度的线程,然后尝试停止所有的正在执行,或任务暂停的线程,并返回等待执行任务的列表;

shutdownNow(),是遍历线程池中的工作线程,然后逐个调用线程的 interrupt,来中断线程,因为如果是无法响应 interupt 的线程,可能永远无法终止;

shutdown 和 shutdownNow,如果两者调用了其一,isShutdown() 方法就会返回 true,当所有的线程都结束后,才表示线程池关闭成功,这时 isTerminaed() 就会返回 true;

线程池的监控

线程池提供了一些监控的属性值,用来堆线程池进行监控; // 可监控的属性值 taskCount :表示线程池需要执行的任务数量 completedTaskCount :表示线程池已执行完成的任务数量(<=taskCount) largestPoolSize :表示线程池曾创建过的最大线程数 activeCount :表示存活的线程数

继承线程池,重写其 beforeExecute,afterExecute,terminated 方法,就可以在任务执行前,执行后,和线程池关闭前,织入一些逻辑,如监控任务的平均执行时间,最大执行时间,和最小执行时间等;

源码分析,参考:https://www.cnblogs.com/fixzd/p/9253203.html;

最新回复(0)