0%

线程池学习小结

一、为什么使用线程池

1. 能够减少线程切换带来的开销

如果有大量执行时间很短的任务,那么上下文切换带来的时间开销甚至会超过任务执行的时间,这显然是不合理的。而使用线程池就能降低线程创建和销毁造成的损耗。

2. 能够提高响应速度

任务到达时,无需等待线程创建即可立即执行。

3. 提高线程的可管理性

线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

二、线程池的生命周期

线程池有5种状态:
在这里插入图片描述
其生命周期转换如下入所示:
在这里插入图片描述

三、线程池的工作原理

线程池的3个重要参数:核心线程数,最大线程数和阻塞队列容量。

线程池将任务提交和任务执行进行了解耦,用户只需要负责提交,而不用去关心任务执行。 在提交一个任务的时候,会根据线程池目前的状态来触发不同的操作:

  1. 如果当前线程池中正在运行的线程 < 核心线程数: 直接创建线程给任务执行
  2. 如果当前线程池中正在运行的线程 >= 核心线程数:
    • 阻塞队列没满:把任务放进阻塞队列中,正在运行的线程执行完之后会从阻塞队列中拿任务执行,没任务就阻塞(生产者消费者)
    • 阻塞队列满了:
      • 如果当前正在工作的线程数 < 最大线程数,就创建线程去执行任务
      • 如果当前正在工作的线程数 >= 最大线程数,就触发拒绝策略:

拒绝策略

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略是一个接口,其设计如下:
在这里插入图片描述
在这里插入图片描述

四、线程池使用场景

场景1:快速响应用户请求

描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。

分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务

在这里插入图片描述

场景2:快速处理批量任务

描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。

分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量

在这里插入图片描述

[Reference]

五、线程池的几种类型

1. FixedThreadPool

a. 特点

FixedThreadPool 中创建了固定个数的线程,其 核心线程数 == 最大线程数,且阻塞队列长度为Integer.MAX_VALUE(就是一个LinkedBlockingQueue).

b. 创建方法

1
2
3
4
5
6
7
8
9
/**
* 创建一个可重用固定数量线程的线程池
*/
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}

c. 使用须知

  • 因为阻塞队列是LinkedBlockingQueue, 因此,所以几乎不会触发拒绝策略,如果提交任务数过多的话,可能会导致OOM.

2. SingleThreadExecutor

a. 特点

线程池中只有1个线程,并保证恒有一个线程(就是这个线程如果挂了,还会创建1个,保证线程池中有1个线程) 核心线程数 == 最大线程数 == 1 ,其他参数和FixedThreadPool相同

b. 创建方法

1
2
3
4
5
6
7
8
9
10
/**
*返回只有一个线程的线程池
*/
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}

c. 使用须知

和FixedThreadPool相同,可能会导致OOM

2. CachedThreadPool

a. 特点

CachedThreadPool 是一个会根据需要创建新线程的线程池。 什么叫根据需要,其实就是,如果一个线程一段时间没有使用的话,就给销毁掉.. 它的实现方法其实就是 核心线程数 == 0, 最大线程数 == Integer.MAX_VALUE,

keepAliveTime是线程池中空闲线程等待工作的超时时间。

当线程池中线程数量大于corePoolSize(核心线程数量)或设置了allowCoreThreadTimeOut(是否允许空闲核心线程超时)时,线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。

因此,如果核心线程数为0的话,只要创建的线程空闲了keepAliveTime,就会被销毁..

b. 创建方法

1
2
3
4
5
6
7
8
9
/**
* 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。
*/
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}

CachedThreadPoolcorePoolSize 被设置为空(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。