如何优雅的使用线程池

182 阅读5分钟

前言

线程池我们在实际的项目开发中应用的比较频繁,那么在实际生产应用中,我们需要注意那些事项呢?本文将对于线程池的使用进行重点的讲解。

合理定义线程池

禁止使用默认线程池

默认的线程池申明指的是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 的监控端点中,就可以在可视化页面查看当前的线程池状态信息。

总结

本文对于线程池的使用注意事项进行详细的讲解,如有疑问请随时反馈。