1. 什么是线程池
线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。
线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。
线程池维护多个线程,等待监督管理者分配可并发执行的任务。
- 一方面避免了处理任务时创建销毁线程开销的代价
- 另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
当然,使用线程池可以带来一系列好处:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor ,就允许任务延期执行或定期执行。
2. 线程池解决的问题是什么
线程池解决的核心问题就是资源管理问题。
在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。
在计算机领域中的表现为:统一管理IT资源,包括服务器、存储、和网络资源等等。通过共享资源,使用户在低投入中获益。除去线程池,还有其他比较典型的几种使用策略包括:
- 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
- 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
- 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。
3. 线程池参数
自定义线程池参数如下:
- 核心线程数(corePoolSize):线程池中一直保持活动的线程数。可以使用 corePoolSize 方法来设置。一般情况下,可以根据系统的资源情况和任务的特性来设置合适的值。
- 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。可以使用maximumPoolSize 方法来设置。如果所有线程都改处于活动状态,而此时又有新的任务提交,线程池会创建新的线程,直到达到最大线程数。
- 非核心线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,如果这些线程在一定时间内没有执行任务,则这些线程会被销毁。可以使用 keepAliveTime 和 TimeUnit 方法来设置。
- 非核心线程存活时间单位(unit):线程池中非核心线程保持存活的时间的单位,通常是 TimeUnit 类的实例,如 TimeUnit.SECONDS。
- 阻塞队列(workQueue):线程池等待队列,维护着等待执行的Runnable对象。当运行当线程数= corePoolSize时,新的任务会被添加到workQueue中,如果workQueue也满了则尝试用非核心线程执行任务,等待队列应该尽量用有界的。
- 线程工厂(threadFactory):用于创建线程的工厂。可以通过实现ThreadFactory接口自定义线程的创建逻辑。
- 拒绝策略(rejectedExecutionHandler):当线程池无法接受新的任务时,会根据设置的拒绝策略进行处理。常见的拒绝策略有AbortPolicy、DiscardPolicy、DiscardOldestPolicyi和CallerRunsPolicy
4.常用的线程池
五种常用的线程池
- Java 定义了 Executor 接口并在该接口中定义了 execute() 用于执行一个线程任务,然后通过ExecutorService 接口实现 Executor 接口并执行具体的线程操作;
- ExecutorService 接口有多个实现类可用于创建不同的线程池,如表所示是5种常用的线程池
4.1、newCachedThreadPool(可缓存的线程池)
- newCachedThreadPool 用于创建一个可缓存线程池,之所以叫可缓存线程池,是因为它在创建新线程时如果有可重用的线程,则重用它们,否则创建一个新线程并将其添加到线程池中;
- 在线程池的 keepAliveTime 时间超过默认的60秒后,该线程会被终止并从缓存中移除,因此在没有线程任务运行时,newCachedThreadPool 将不会占用系统的线程资源;
- 在有执行时间很短的大量任务需要执行的情况下,newCachedThreadPool 能很好地复用运行中的线程资源来提高系统的运行效率;
ExecutorService pool = Executors.newCachedThreadPool();
4.2、newFixedThreadPool(固定大小的线程池)
- newFixedThreadPool 用于创建一个固定线程数量的线程池;
- 如果任务数量大于等于线程池中线程的数量,则新提交的任务将在阻塞队列中排队,直到有可用的线程资源;
ExecutorService pool = Executors.newFixedThreadPool(10);
4.3、newScheduledThreadPool(可做任务调度的线程池)
newScheduledThreadPool用于创建可定时调度的线程池,可设置在给定延迟时间后执行或定期执行某个线程任务。
public class Test{
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
// 创建一个延迟3秒执行的线程
pool.schedule(new Runnable() {
public void run() {
System.out.println("delay 3 seconds" + Thread.currentThread().getName());
}
}, 3, TimeUnit.SECONDS);
// 创建一个延迟3秒且每1秒执行一次的线程
pool.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println("delay 3 second and repeat execute every 1 seconds" + Thread.currentThread().getName());
}
}, 3, 1, TimeUnit.SECONDS);
// 关闭线程池
// pool.shutdown();
}
}
效果
delay 3 secondspool-1-thread-1
delay 3 second and repeat execute every 1 secondspool-1-thread-2
delay 3 second and repeat execute every 1 secondspool-1-thread-2
delay 3 second and repeat execute every 1 secondspool-1-thread-2
4.4、newSingleThreadPool(单个线程的线程池)
- newSingleThreadPool 创建的线程池会确保池中永远有且只有一个可用的线程;
- 在该线程停止或发生异常时,newSingleThreadPool 线程池会启动一个新的线程代替该线程继续执行任务;
ExecutorService pool = Executors.newSingleThreadExecutor();
4.5、newWorkStealingPool(足够大小的线程池)
- newWorkStealingPool 用于创建持有足够线程的线程池来达到快速运算的目的;
- 在内部通过使用多个队列来减少各个线程调度产生的竞争;
- 足够的线程指JDK根据当前线程的运行需求向操作系统申请足够的线程,以保障线程的快速执行;
// 创建一个工作窃取线程池,线程数量默认为当前机器处理器核心数量
ExecutorService executorService = Executors.newWorkStealingPool();
5. 线程池调优
5.1确定核心线程数
执行线程池执行任务的类型
- IO密集型任务:一般来说:文件读写、DB读写、网络请求等
- CPU密集型任务:一般来说:计算型代码、Bitmap转换、Gson转换等
System.out.println(Runtime.getRuntime().availableProcessors());
① 高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
- IO密集型的任务 --> (CPU核数 * 2 + 1)
- 计算密集型任务 --> ( CPU核数+1 )
③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考
5.2 调优思路
参数说明
- QPS是每秒的访问数
- TP99是99%网络请求需要的最低耗时
- Buffer是还能给到的线程数
优思路
- 线程池调优主要要根据实际业务场景去进行调整,一般调整参数主要是核心线程数、最大线程数、阻塞队列以及拒绝策略
- 如何设置核心线程数,可以从响应时间和吞吐量敏感度考虑
- 对于C端场景需要考虑响应时间,对于批量调用下游服务的场景,可以合理配置核心线程数让任务尽早的执行完
- hystrix官网建议,核心线程数 = 下游任务QPS * 下游任务TP99 + 冗余线程数Buffer
- 对于B端场景需要考虑吞吐量,对于使用MQ、Job进行任务处理时,由于不考虑立刻响应,核心线程数够用即可,可以让尽可能多的任务阻塞在队列中,少占用CPU资源
- 对于C端场景需要考虑响应时间,对于批量调用下游服务的场景,可以合理配置核心线程数让任务尽早的执行完
- 对于最大线程数,通常情况下不要超过CPU核心数,因为超过了可能达不到提高系统的并发能力,反而会增加线程切换的开销,可以根据系统的负载情况和任务的处理时间,可以预估出系统需要的最大线程数
- 如何设置核心线程数,可以从响应时间和吞吐量敏感度考虑
- 另外对于线程池也需要考虑加监控(比如告警),监控任务执行时间、活跃线程数、队列中任务数等,用于上线评估,最好可以核心线程数可配置化来应对大促,对于拒绝策略可以根据业务、监控情况来设计
- 对于线程池的监控,执行任务的时候可以将当前线程池对象的参数打印出来,然后通过三方平台将日志解析出来,通过数据看板的形式来监控,如果发现问题,就及时告警出来
- 另外可以参考美团动态线程池的设计思想,通过 JDK 提供的几个核心的设置方法通过不同策略去进行动态调整
- 对于线程池的监控,执行任务的时候可以将当前线程池对象的参数打印出来,然后通过三方平台将日志解析出来,通过数据看板的形式来监控,如果发现问题,就及时告警出来
参数参考
- C端
- 10核心线程数、20最大线程数、最大空闲时间60s、采用2000容量的LinkedBlockingDeque
- B端 MQ、Job
- 1核心线程数、2最大线程数、最大空闲时间60s,采用200000容量的LinkedBlockingDeque