一、为什么使用线程池
1. 能够减少线程切换带来的开销
如果有大量执行时间很短的任务,那么上下文切换带来的时间开销甚至会超过任务执行的时间,这显然是不合理的。而使用线程池就能降低线程创建和销毁造成的损耗。
2. 能够提高响应速度
任务到达时,无需等待线程创建即可立即执行。
3. 提高线程的可管理性
线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
二、线程池的生命周期
线程池有5种状态:
其生命周期转换如下入所示:
三、线程池的工作原理
线程池的3个重要参数:核心线程数,最大线程数和阻塞队列容量。
线程池将任务提交和任务执行进行了解耦,用户只需要负责提交,而不用去关心任务执行。 在提交一个任务的时候,会根据线程池目前的状态来触发不同的操作:
- 如果当前线程池中正在运行的线程 < 核心线程数: 直接创建线程给任务执行
- 如果当前线程池中正在运行的线程 >= 核心线程数:
- 阻塞队列没满:把任务放进阻塞队列中,正在运行的线程执行完之后会从阻塞队列中拿任务执行,没任务就阻塞(生产者消费者)
- 阻塞队列满了:
- 如果当前正在工作的线程数 < 最大线程数,就创建线程去执行任务
- 如果当前正在工作的线程数 >= 最大线程数,就触发拒绝策略:
拒绝策略
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
拒绝策略是一个接口,其设计如下:
四、线程池使用场景
场景1:快速响应用户请求
描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。
分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。
场景2:快速处理批量任务
描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。
分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
五、线程池的几种类型
1. FixedThreadPool
a. 特点
FixedThreadPool 中创建了固定个数的线程,其 核心线程数 == 最大线程数,且阻塞队列长度为Integer.MAX_VALUE(就是一个LinkedBlockingQueue).
b. 创建方法
1 | /** |
c. 使用须知
- 因为阻塞队列是LinkedBlockingQueue, 因此,所以几乎不会触发拒绝策略,如果提交任务数过多的话,可能会导致OOM.
2. SingleThreadExecutor
a. 特点
线程池中只有1个线程,并保证恒有一个线程(就是这个线程如果挂了,还会创建1个,保证线程池中有1个线程) 核心线程数 == 最大线程数 == 1 ,其他参数和FixedThreadPool相同
b. 创建方法
1 | /** |
c. 使用须知
和FixedThreadPool相同,可能会导致OOM
2. CachedThreadPool
a. 特点
CachedThreadPool
是一个会根据需要创建新线程的线程池。 什么叫根据需要,其实就是,如果一个线程一段时间没有使用的话,就给销毁掉.. 它的实现方法其实就是 核心线程数 == 0, 最大线程数 == Integer.MAX_VALUE,
keepAliveTime是线程池中空闲线程等待工作的超时时间。
当线程池中线程数量大于corePoolSize(核心线程数量)或设置了allowCoreThreadTimeOut(是否允许空闲核心线程超时)时,线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。
因此,如果核心线程数为0的话,只要创建的线程空闲了keepAliveTime,就会被销毁..
b. 创建方法
1 | /** |
CachedThreadPool
的corePoolSize
被设置为空(0),maximumPoolSize
被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 maximumPool
中线程处理任务的速度时,CachedThreadPool
会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。
c. 使用须知
因为最大线程数没有上线,因此极端情况也会OOM
3. ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor
主要用来在给定的延迟后运行任务,或者定期执行任务。
六、确定线程池大小
具体问题具体分析,一个简单且适用面儿比较光的场合:
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。