什么是线程池

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.常用的线程池

五种常用的线程池

  1. Java 定义了 Executor 接口并在该接口中定义了 execute() 用于执行一个线程任务,然后通过ExecutorService 接口实现 Executor 接口并执行具体的线程操作;
  2. 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资源
    • 对于最大线程数,通常情况下不要超过CPU核心数,因为超过了可能达不到提高系统的并发能力,反而会增加线程切换的开销,可以根据系统的负载情况和任务的处理时间,可以预估出系统需要的最大线程数
  • 另外对于线程池也需要考虑加监控(比如告警),监控任务执行时间、活跃线程数、队列中任务数等,用于上线评估,最好可以核心线程数可配置化来应对大促,对于拒绝策略可以根据业务、监控情况来设计
    • 对于线程池的监控,执行任务的时候可以将当前线程池对象的参数打印出来,然后通过三方平台将日志解析出来,通过数据看板的形式来监控,如果发现问题,就及时告警出来
      • 另外可以参考美团动态线程池的设计思想,通过 JDK 提供的几个核心的设置方法通过不同策略去进行动态调整

参数参考

  • C端
    • 10核心线程数、20最大线程数、最大空闲时间60s、采用2000容量的LinkedBlockingDeque
  • B端 MQ、Job
    • 1核心线程数、2最大线程数、最大空闲时间60s,采用200000容量的LinkedBlockingDeque
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值