关闭线程池
可调用线程池的shutdown或shutdownNow方法关闭线程池。
它们都是遍历线程池中的工作线程,然后逐个调用线程的**interrupt()**来中断线程,所以无法响应中断的任务可能永远无法终止。
- shutdownNow
首先将线程池的状态设成STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表 - shutdown
只是将线程池的状态设成SHUTDOWN态,然后中断所有没有正在执行任务的线程
只要调用了这两个关闭方法中的任一,isShutdown() 就会返回true。当所有任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。
至于应该调用哪一种方法,应该由提交到线程池的任务的特性决定:
- 通常调用shutdown() 关闭线程池
- 若任务不一定要执行完,则可以调用shutdownNow
合理配置
要想合理地配置线程池,就必须首先
分析任务特性
可从以下几个角度来分析
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务
- 任务的优先级:高、中和低
- 任务的执行时间:长、中和短
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务
可以用不同规模的线程池分开处理
CPU密集型任务(计算型任务)
吞吐量较大的计算型任务,线程数量应该较少,可为N(CPU核数)+1或 N(CPU核数) * 2,因为此时线程一定调度到某个CPU执行,若任务本身是CPU绑定的任务,那么过多的线程只会增加线程切换的开销,而不能提升吞吐量,但可能需要较长队列做缓冲。
I/O密集型任务
执行较慢、数量不大的IO任务,要考虑更多线程数,而无需太大队列。
相比计算型任务,需多一些线程,要结合具体的 I/O 阻塞时长考虑
如Tomcat中默认的最大线程数为: 200。
也可考虑根据需要在一个最小数量和最大数量间弹性地自动增减线程数。
业务读取较多,线程并不是一直在执行任务,则应配置尽可能多的线程
N(CPU)/1 - 阻塞系数(0.8~0.9)
一般生产环境下的CPU使用率达到80,说明充分利用了。
混合型的任务
如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量.如果这两个任务执行时间相差太大,则没必要进行分解.
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数.
优先级不同的任务可以使用PriorityBlockingQueue处理.它可以让优先级高
的任务先执行.
注意 如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行.
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU.
建议使用有界队列 有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千.
假如系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里.
如果我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题.
复用线程池不代表应用程序始终使用同一个线程池,我们应该根据任务的性质来选用不同的线程池。特别注意IO绑定的任务和CPU绑定的任务对于线程池属性的偏好,如果希望减少任务间的相互干扰,考虑按需使用隔离的线程池。
2.5 线程池的监控
如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题.可通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性:
- taskCount:线程池需要执行的任务数量
- completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
- largestPoolSize:线程池里曾经创建过的最大线程数量.通过这个数据可以知道线程池是否曾经满过.如该数值等于线程池的最大大小,则表示线程池曾经满过.
- getPoolSize:线程池的线程数量.如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减.
- getActiveCount:获取活动的线程数.
通过扩展线程池进行监控.可以通过继承线程池来自定义线程池,重写线程池的
beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控.例如,监控任务的平均执行时间、最大执行时间和最小执行时间等.
这几个方法在线程池里是空方法.
/
protected void beforeExecute(Thread t, Runnable r) { }
线程池作为应用程序内部的核心组件往往缺乏监控,往往到程序崩溃后才发现线程池的问题,很被动。
2.6 线程池的状态
1.当线程池创建后,初始为 running 状态
2.调用 shutdown 方法后,处 shutdown 状态,此时不再接受新的任务,等待已有的任务执行完毕
3.调用 shutdownnow 方法后,进入 stop 状态,不再接受新的任务,并且会尝试终止正在执行的任务。
4.当处于 shotdown 或 stop 状态,并且所有工作线程已经销毁,任务缓存队列已清空,线程池被设为 terminated 状态。
总结
线程池有哪些关键属性?
workQueue 用于存放任务,添加任务的时候,如果当前线程数超过了 corePoolSize,那么往该队列中插入任务,线程池中的线程会负责到队列中拉取任务。
keepAliveTime 用于设置空闲时间,如果线程数超出了 corePoolSize,并且有些线程的空闲时间超过了这个值,会执行关闭这些线程的操作
rejectedExecutionHandler 用于处理当线程池不能执行此任务时的情况,默认有抛出 RejectedExecutionException 异常、忽略任务、使用提交任务的线程来执行此任务和将队列中等待最久的任务删除,然后提交此任务这四种策略,默认为抛出异常。
线程池中的线程创建时机?
如果当前线程数少于 corePoolSize,那么提交任务的时候创建一个新的线程,并由这个线程执行这个任务;
如果当前线程数已经达到 corePoolSize,那么将提交的任务添加到队列中,等待线程池中的线程去队列中取任务;
如果队列已满,那么创建新的线程来执行任务,需要保证池中的线程数不会超过 maximumPoolSize,如果此时线程数超过了 maximumPoolSize,那么执行拒绝策略。
任务执行时发生异常怎么办?
若某任务执行出现异常,则执行任务的线程会被关闭,而不是继续接收其他任务。然后会启动一个新的线程来代替它。
何时执行拒绝策略?
workers 的数量达到了 corePoolSize,任务入队成功,以此同时线程池被关闭了,而且关闭线程池并没有将这个任务出队,那么执行拒绝策略。这里说的是非常边界的问题,入队和关闭线程池并发执行,读者仔细看看 execute 方法是怎么进到第一个 reject(command) 里面的。
- workers 的数量大于等于 corePoolSize,准备入队,可是队列满了,任务入队失败,那么准备开启新的线程,可是线程数已经达到 maximumPoolSize,那么执行拒绝策略
线程池过多造成OOM 因为活跃线程过多和线程池不会被回收
Java Stream Api异步分流 公用一个默认forkjion线程池,使用时要注意
线程池创建时要分析执行任务是IO资源型还是CPU资源型
IO资源型或者说执行较长时间任务,并且拒绝策略为Call时,会在线程池满状态后交给调用者线程执行,如果是Web服务跑在tomcat,就导致整体吞吐量下降
参考
- 《码出高效》
- 《Java并发编程的艺术》
https://www.zhihu.com/question/23212914/answer/140255498