Java的ThreadPoolExecutor业务线程池详细解析
作者:岸河
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<Runnable> 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使用。
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;
经典面试题
线程池什么时候创建核心线程,什么时候把任务放进阻塞队列,什么时候创建空闲线程?
答:任务刚开始进来的时候就创建核心线程,核心线程满了会把任务放到阻塞队列,阻塞队列满了之后才会创建空闲线程,达到最大线程数之后,再有任务进来,就只能执行拒绝策略了。
注意,执行拒绝策略有两个场景,一个是空闲线程也满了,二是线程池不在运行了,比如执行了shutdown的方法,但是这个时候又来了新任务。
基础知识
现阻塞队列的接口是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的值是可以设置的。利用这点加上配置中心,可以动态的调整核心线程数。
四、线程池总结
做好业务线程池,分三个级别
第一级别,根据业务特性实现不同的业务线程池。
第二级别,根据业务特性,动态调整线程池配置。
第三级别,实时监控与配置线程池运行情况。
到此这篇关于Java的ThreadPoolExecutor业务线程池详细解析的文章就介绍到这了,更多相关ThreadPoolExecutor业务线程池内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!