2023-深入理解Java线程池:线程池参数调优与技巧

深入理解Java线程池:线程池参数调优与技巧

在现代编程中,线程池已经成为了不可或缺的一部分。Java线程池是一个非常重要的组件,可以帮助我们优化并发处理,提高系统的性能和稳定性。然而,要想取得优秀的性能表现,需要对线程池的参数进行调优。本文将深入讲解Java线程池的调优方法和技巧,帮你提高编程技能和优化系统性能。

线程池简介

线程池概述

线程池是一种管理和重用线程资源的机制。通常情况下,每个任务都需要一个独立的线程来执行。当任务变得越来越多,每个任务都创建一个新线程就会导致系统负荷过重。这时候线程池的使用就能很好地解决这个问题。

线程池维护了一组空闲线程,当有任务需要执行时,就从线程池中选择一个空闲的线程执行任务,当任务执行完成后,这个线程就会被重新放回线程池,供下一次任务使用,这样可以节省线程创建和销毁的时间成本,提高系统的执行效率和响应速度。

线程池API介绍

Java中的线程池实现在java.util.concurrent包下,主要有以下几个类:

  • Executor:线程池顶层接口,定义了线程池的execute()方法,提交任务到线程池去执行。
  • ExecutorService:线程池的具体实现接口,通过submit()方法向线程池提交任务,返回Future,可以通过Future获得任务的执行情况。
  • ScheduledExecutorService:实现类似于Timer的计划任务调度功能。
  • ThreadPoolExecutor:是ExecutorService的一个实现类,也是Java中的线程池实现的核心实现类。通常情况下,我们是使用ThreadPoolExecutor来实现线程池功能。

这些API提供以下四种类型的线程池:

  • FixedThreadPool -它包含固定数量的线程启动,线程数设定的越多并发就越高,内部的任务队列FIFO(先进先出)
  • CachedThreadPool -这个池默认规定的池大小(线程数)是没有限制的,比较适合执行线程简小且IO密集型或计算密集型task,避免等待I/O浪费时间(wait)来提供更大的利用率调用超过线程数的taskthread中。
  • SingleThreadExecutor - SingleThreadExecutor是指有个线程池保持单线程工作。它用于背后处理活动在一个job queue中不大并且涉及器不借此启动新线程,避免上下文切换(set Context_Switches)的操作开销。
  • ScheduledThreadPool -允许用户执行时间的安排或周期性任务在future类型值上的结果。以可调类型提供相对 延迟(取代sleep)fixed-delay并符合UTC执行时间的时间单元作为其参数(一些days、hours、 minutes、seconds等等)

线程池的用法

Java中线程池的创建步骤一般为以下三步:

  1. 创建线程池执行器:可以通过Executor框架提供的工厂方法或通过ThreadPoolExecutor构造函数传入参数的方式来进行创建。
  2. 提交任务:通过使用submit()方法将多个任务提交到线程池中等待执行。
  3. 关闭线程池:可以手动关闭线程池、也可以使用shutdown()shutdownNow()方法关闭线程池。注意关闭线程池需要注意已经提交到线程池中的任务是否全部完成。

线程池常用参数含义

  1. corePoolSize (核心线程池大小)

该参数指定核心线程池中线程的数量。当提交一个新任务时,如果当前线程池中的线程数少于corePoolSize,那么就会创建新的线程。即使其他空闲的非核心线程可以处理新任务,也会继续创建线程,达到核心线程池大小。如果设置为0,则任务会不断地加入队列,并在工作线程可用时立即执行。

  1. maximumPoolSize(最大线程池大小)

该参数指定总线程池大小,包括核心线程池和非核心线程池。在任务队列满了的情况下,可以创建的最大线程数。如果此时运行的线程数已经等于了maximumPoolSize,则提交的任务会根据选择的拒绝策略进行处理。

  1. keepAliveTime(线程存活时间)

当线程池中的数量大于corePoolSize时,这是多余的空闲线程的最长存活时间。直到线程数等于corePoolSize,超过这个时间,空闲线程就会被回收。

  1. workQueue(任务队列)

任务队列是存储被提交但尚未被执行的任务的阻塞队列。常用的任务队列有如下几类:

  • ArrayBlockingQueue:基于数组的有限队列,可以指定容量。
  • LinkedBlockingQueue:基于链表的无限队列,可以无限扩展。
  • PriorityBlockingQueue:优先级队列,可以自定义排列顺序。
  • SynchronousQueue:同步队列,不存储数据,只在提交和取出数据时传递数据。
  1. RejectedExecutionHandler(拒绝策略)

拒绝策略是当任务队列满了需要执行拒绝策略来处理新提交的任务。提供了几种预定义的拒绝策略:

  • AbortPolicy:直接抛出异常,默认策略。
  • CallerRunsPolicy:主线程执行该任务。
  • DiscardOldestPolicy:丢弃队列中最老的任务,然后重新尝试执行当前任务。
  • DiscardPolicy:默默丢弃提交的任务,没有异常。

线程池调优

线程池的合理性评估

在开始调整线程池之前,需要对当前的系统进行全面的评估。可以通过使用系统监控软件,来确定系统的各项指标如CPU使用率、内存使用率、磁盘I/O等等,以及确定系统瓶颈。此外还要考虑到系统的上下文切换的开销。

如何选择合适的参数值

  1. 核心线程池大小

核心线程池大小应该根据需要处理的并发任务数以及CPU核心数来确定。通常来说,核心线程数可以设为CPU核心数+ 1

  1. 最大线程池大小

最大线程池大小应该根据系统应对的最高并发数来确定。如果最大线程池大小比较大,会导致系统资源的浪费;如果比较小,会导致请求被拒绝。建议根据硬件资源、负载、并发量等实际情况来确定。

  1. 存活时间

存活时间通常设置为60s左右,即当线程池中的线程空闲时间超过了60秒,那么这个线程就会被回收。同时,当线程池中的线程数量小于等于核心线程池大小时,存活时间将不起作用。

  1. 队列选择

常用的阻塞队列有ArrayBlockingQueueLinkedBlockingQueue

  • 如果任务数目大于队列长度,那么将会创建新线程去处理任务。因此队列应该尽量设置为有限阻塞队列,避免无限制的任务添加。
  • 若系统负载较大或并发量较大时,可以使用LinkedBlockingQueue,因为LinkedBlockingQueue可以无限制存储任务,防止任务丢失。
  • 若任务量较小,建议使用ArrayBlockingQueue,因为存储数组有限,有利于反馈当前任务的处理情况。
  1. 拒绝策略

拒绝策略有四种:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。其中,DiscardOldestPolicy可以尽量取出能够处理的任务,而不是将任务全部丢弃。如果需要响应时间更好,可以使用CallerRunsPolicy。若任务量极高,建议使用DiscardPolicy策略。

线程池及线程池大小调优技巧

  1. 核心线程池数量调优

核心线程池数量不宜过多,因为每个线程都需要占用内存和CPU资源,过多的核心线程池数量会导致系统资源的浪费,从而降低系统性能。但也不应过少,否则会降低系统吞吐量。

  1. 最大线程池数量调优

最大线程池数量一般设置:最大线程池数量= CPU核心数+网络连接数+其他IO等待时间的线程数量

  1. 存活时间调优

设定一个适当的线程存活时间,可以有效地减少线程的创建和销毁带来的性能开销。在存活时间到达之后,多余的线程会被回收,从而释放系统资源。

  1. 队列调优

任务队列是存储被提交但尚未被执行的任务的阻塞队列。在选择队列类型时,应考虑任务数量和任务类型,以及需要处理的并发请求数。常用的阻塞队列有ArrayBlockingQueueLinkedBlockingQueue

  1. 拒绝策略调优

拒绝策略通常分为四种:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicyDiscardPolicy。根据业务需求和系统负载情况,选择合适的拒绝策略。

应用场景

  1. CPU密集型应用线程池设置:

CPU密集型应用中,任务主要是CPU计算,线程池的大小应该根据CPU核心数来设置,以充分利用CPU资源,并避免过多线程间的竞争和上下文切换。通常情况下,将核心线程池大小设置为CPU核心数,将最大线程池大小设置为CPU核心数* 2。比如,当前服务器有8CPU,那么推荐设置核心线程池大小为8,最大线程池大小为16

  1. IO密集型应用线程池设置:

IO密集型应用中,任务主要是从事IO等待,而线程的CPU计算能力却很小,此时线程数量适当多一点,可以让CPU等待IO的数目更多,以充分利用计算机的硬件资源。通常情况下,核心线程池大小可以设置为CPU核心数+1,最大线程池大小可以设置为CPU核心数* 2。同时,建议使用无界的LinkedBlockingQueue阻塞队列,以避免丢失任务。

  1. 其它应用场景:

对于各种不同的场景,应该根据实际情况进行参数的设置。比如生产者-消费者问题,可以使用FixedThreadPool,保证消费者线程数量少于核心线程池大小,以确保消费者线程能够及时执行。同时,阻塞队列也可以根据实际情况选择不同的存储方式。对于需要开多个线程处理的应用,可以使用ScheduledThreadPoolExecutor,定时执行任务。总之,根据实际需求来设置线程池的参数是最重要的。

总结

  1. 线程池参数设置的思考

线程池对于提高系统性能和可管理性非常重要,线程池的性能和效率很大程度上取决于参数的设置。对于不同的业务和场景,需要根据实际情况来设置线程池的参数。

  • 核心线程池大小和最大线程池大小应该根据实际需求来设置,一般建议把核心线程池大小设置成CPU核心数加1,而最大线程池大小可以设置为CPU核心数乘2
  • 阻塞队列的大小和类型应该根据实际情况来设置,不同类型的阻塞队列对于不同的场景都有其优缺点,比如ArrayBlockingQueue适用于任务执行比较平缓的场景,而LinkedBlockingQueue则适用于任务执行频繁的场景。
  • 线程存活时间应该根据实际情况来设置,一般建议设置为60s左右。
  • 拒绝策略应该根据实际情况来设置,比如当线程池中的队列已满或达到了最大线程数时,应该使用合适的拒绝策略。
  1. 注意事项:
  • 线程池中的线程数量不是越多越好,过多的线程数量会增加线程上下文切换的开销,同时也会占用过多的系统资源。
  • 线程池的大小应该根据实际情况来设置,不同的场景和业务具有不同的特点,需要根据实际情况做出合理的选择。
  • 阻塞队列的大小和类型应该根据实际情况做出选择,为了防止任务丢失,应该将其设置成有界队列,同时也需要考虑队列的容量大小和类型。
  • 线程池的拒绝策略应该根据实际情况来选择,不同的业务场景具有不同的特点,需要做出合理的选择。