Java线程池 ThreadPoolExecutor 业务线程池

1.什么是业务线程池?

在业务开发中,用来处理业务的线程池。

2.为什么需要业务线程池?

大多数同学都是做业务开发的,很多业务的操作并非要求一定是同步的。例如,对于一系列连续的业务逻辑处理,很多都是数据的组装,拼接,查询,或者将数据同步给各个下层业务(对事务性没有严格要求);或者对数据的批量操作;这些都可以是异步的。通常业务项目使用的都是的servlet框架,都是使用一个线程进行业务逻辑处理,这种模型是通用的,但不一定是最佳的,不一定是最适合的。需要我们业务开发者根据实际的业务场景去灵活应用,达到最快的响应,最大的吞吐量。

3.业务线程池应用的思路是来自哪里?

个人理解,来自于开源框架。各种池化的概念,太多了,线程池,内存池,实例池,连接池。太多框架使用了线程池的概念,spring,tomcat,dubbo,netty,rocketmq,nacos,druid,总而言之,几乎所有的框架,都用到了线程池。虽然他们是框架线程池,但是抽出来想一下,对于框架线程池来讲,我们对于框架的使用,也是业务流程,也需要业务逻辑的处理,因此,业务线程池,框架线程池,两者并无区别。

一、业务线程池的好处

这里借用《Java 并发编程的艺术》提到的来说一下 使用线程池的好处

  • 降低资源消耗 。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度 。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性 。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

二、线程池基本认识

参数说明

/**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {

        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

拒绝策略

  • AbortPolicy:直接抛出异常,这是默认策略;
  • CallerRunsPolicy:用调用者所在的线程来执行任务;(这种场景下,可以保证数据不丢失,但是会阻塞主线程)
  • DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  • DiscardPolicy:直接丢弃任务;

ExecutorService 中 shutdown()、shutdownNow()、awaitTermination() 含义和区别

  • shutdown():停止接收新任务,原来的任务继续执行
  • shutdownNow():停止接收新任务,原来的任务停止执行
  • awaitTermination(long timeOut, TimeUnit unit):当前线程阻塞

注意:

awaitTermination一般是配合shutdown使用。

参考文章

https://blog.csdn.net/xiaojin21cen/article/details/81778651

ThreadPoolExecutor运行状态

ThreadPoolExecutor类中定义了5个Integer常量,状态分别为

// runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
Java线程池 ThreadPoolExecutor 业务线程池

经典面试题

  1. 线程池什么时候创建核心线程,什么时候把任务放进阻塞队列,什么时候创建空闲线程?答:任务刚开始进来的时候就创建核心线程,核心线程满了会把任务放到阻塞队列,阻塞队列满了之后才会创建空闲线程,达到最大线程数之后,再有任务进来,就只能执行拒绝策略了。注意,执行拒绝策略有两个场景,一个是空闲线程也满了,二是线程池不在运行了,比如执行了shutdown的方法,但是这个时候又来了新任务。

基础知识

  1. 实现阻塞队列的接口是BlockingQueue,jdk1.5新增的,在juc包下面,作者是Doug Lea,它的父接口是Queue,也是jdk1.5新增的,在java.util包下面,属于集合类,作者还是Doug Lea。

三、线程池最佳实践

1.打印线程池的状态,关注线程池运行情况(个人非常喜欢)

/**
     * 打印线程池的状态
     *
     * @param threadPool 线程池对象
     */
    public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {

        ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-thread-pool-status", false));
        scheduledExecutorService.scheduleAtFixedRate(() -> {

            log.info("=========================");
            log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());
            log.info("Active Threads: {}", threadPool.getActiveCount());
            log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());
            log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
            log.info("=========================");
        }, 0, 1, TimeUnit.SECONDS);
    }

2.不同业务使用不同的业务线程池

父子任务也不要使用一个线程池(会发生死锁),死锁原因:父任务占用了所有的核心线程,自子任务在阻塞队列里等待父任务释放核心线程,父线程等待子任务完成任务。

3.为什么不能使用原生的Executors工具创建线程池

阻塞队列都是Integer.MAX,容易发生OOM,而且无线程池命名,没有关心空闲时间,拒绝策略,太粗糙了,除非你不关心业务。

4.如果设置线程数量?

有一个简单并且适用面比较广的公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

5.[美团] Java线程池实现原理及其在美团业务中的实践

  • 由于队列设置过长,最大线程数设置失效,导致请求数量增加时,大量任务堆积在队列中,任务执行时间过长,最终导致下游服务的大量调用超时失败。
  • ThreadPoolExecutor的corePoolSize的值是可以设置的。利用这点加上配置中心,可以动态的调整核心线程数。

参考文章:

  • https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&mid=2247505103&idx=1&sn=a041dbec689cec4f1bbc99220baa7219&source=41#wechat_redirect
  • https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

四、线程池总结

做好业务线程池,分三个级别

第一级别,根据业务特性实现不同的业务线程池。

第二级别,根据业务特性,动态调整线程池配置。

第三级别,实时监控与配置线程池运行情况。

五、参考

  • https://zhuanlan.zhihu.com/p/147627756
  • https://www.zhihu.com/question/412524104

你可能感兴趣的:(Java线程池 ThreadPoolExecutor 业务线程池)