前言
线程池我们在实际的项目开发中应用的比较频繁,那么在实际生产应用中,我们需要注意那些事项呢?本文将对于线程池的使用进行重点的讲解。
合理定义线程池
禁止使用默认线程池
默认的线程池申明指的是Executors工具类中给我提供了newFixedThreadPool、SingleThreadPool、newCachedThreadPool、scheduledThreadPool这四种申明线程池的方法,但是实际的生产中不建议直接使用,具体原因如下:
- Executors.newFixedThreadPool 和 Executors.SingleThreadPool 创建的线程池内部使用的是无界的LinkedBlockingQueue队列,会堆积大量请求,导致系统内存溢出。
- Executors.newCachedThreadPool和Executors.scheduledThreadPool创建的线程池最大线程数是用的Integer.MAX_VALUE,可能会创建大量线程,如果没有及时回收,可能会导致系统内存溢出。
所以生产环境通过建议自定义线程池。
合理设置线程池的核心参数
线程池的核心参数通常包括:核心线程数、最大线程数、队列大少等。 如果线程池的线程数量过多,虽然局部处理速度增加,但将会影响应用系统的整体性能。而如果线程池的线程数量过少,线程池可能无法带来预期的性能的提升;所以为了降低这些风险的发生,在设置线程池的类型和参数时,应当格外小心。在正式上线前,需要做一次压力测试,综合进行评估设置。
线程池隔离
对于不同的业务,需要创建不同的线程池来进行业务处理。尽量避免整个服务共享一个全局线程池,导致任务相互影响。
线程池异常处理
使用ThreadPoolExecutor对象的execute()方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止,虽然任务虽然异常了,但是却获取不到任何通知,这会让你误以为任务都执行得很正常。虽然线程池提供了很多用于异常处理的方法,但是最稳妥方案还是捕获所有异常并按需处理。
try {
//业务逻辑
} catch (RuntimeException x) {
//按需处理
} catch (Throwable x) {
//按需处理
}
设定统一线程前缀名,便于跟踪查看及问题排查.
我们在定义线程池时需要统一下前缀,需要实现ThreadFactory接口,具体如下
public class MyThreadFactory implements ThreadFactory
{
private String prefix;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public MyThreadFactory(String prefix)
{
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r)
{
Thread thread =new Thread(null,r,prefix+threadNumber.getAndIncrement());
return thread;
}
}
如何选择合适拒绝策略
线程池为我们提供了四种默认的策略分别为:
- AbortPolicy:直接抛出异常,后续的任务不会执行
- CallerRunsPolicy:子任务执行的时间过长,可能会阻塞主线程。
- DiscardPolicy:不抛异常,任务直接丢弃
- DiscardOldestPolicy;丢弃最老的任务,然后把新任务加入到工作队列
注意:线程池默认的策略为AbortPolicy策略,线程池默认的拒绝策略会throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制catch,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。
如果上述四种策略均不满足,可以通过RejectedExecutionHandler接口定制个性化的拒绝策略。为了兼顾任务不丢失和系统负载,建议实现拒绝策略。
public class CustRejectedExecutionHandler implements RejectedExecutionHandler
{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
{
//保持数据库或者缓存消息队列等
}
}
如何优雅关闭线程池
线程池有运行自然就要关闭,那么如何进行线程池的关闭,我们可以采用其实无非就是两个方法 shutdown()/shutdownNow()。
但这两个方法的区别如下:
- shutdown()执行后停止接受新任务,会把队列的任务执行完毕。
- shutdownNow()也是停止接受新任务,但会中断所有的任务,将线程池状态变为stop。
shutdownNow()简单粗暴,所以需要根据实际场景选择不同的方法。
示例说明:
public class ShutDownTest
{
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
executor.execute(new Task("0"));
Thread.sleep(1);
executor.shutdown();
System.out.println("executor has been shutdown");
}
static class Task implements Runnable
{
String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <= 50 && !Thread.interrupted(); i++) {
Thread.yield();
System.out.println("task " + name + " is running,id:" + i);
}
}
}
}
运行结果:
.....
task 0 is running,id:44
executor has been shutdown
task 0 is running,id:45
task 0 is running,id:46
task 0 is running,id:47
task 0 is running,id:48
task 0 is running,id:49
task 0 is running,id:50
说明:从结果我们可以看出,线程池被关闭后,正则运行的任务没有被interrupt。说明shutdown()方法不会 interrupt运行中线程,如果修改为shutdownNow()来看下执行结果
.....................
task 0 is running,id:41
executor has been shutdown
说明:修改为shutdownNow()后,task任务没有执行完,执行到中间的时候就被interrupt后没有继续执行了。
线程池监控
如果项目采用的是Spring Boot申明的线程池,我们可以采用actuator组件来做线程池的监控。其实 ThreadPool本身已经提供了不少api可以获取线程状态,我们只需要将这些信息暴露到SpringBoot 的监控端点中,就可以在可视化页面查看当前的线程池状态信息。
总结
本文对于线程池的使用注意事项进行详细的讲解,如有疑问请随时反馈。