在现代编程中,线程池已经成为了不可或缺的一部分。Java 线程池是一个非常重要的组件,可以帮助我们优化并发处理,提高系统的性能和稳定性。然而,要想取得优秀的性能表现,需要对线程池的参数进行调优。本文将深入讲解 Java 线程池的调优方法和技巧,帮你提高编程技能和优化系统性能。
线程池是一种管理和重用线程资源的机制。通常情况下,每个任务都需要一个独立的线程来执行。当任务变得越来越多,每个任务都创建一个新线程就会导致系统负荷过重。这时候线程池的使用就能很好地解决这个问题。
线程池维护了一组空闲线程,当有任务需要执行时,就从线程池中选择一个空闲的线程执行任务,当任务执行完成后,这个线程就会被重新放回线程池,供下一次任务使用,这样可以节省线程创建和销毁的时间成本,提高系统的执行效率和响应速度。
Java 中的线程池实现在java.util.concurrent
包下,主要有以下几个类:
Executor
:线程池顶层接口,定义了线程池的 execute()方法,提交任务到线程池去执行。ExecutorService
:线程池的具体实现接口,通过 submit()方法向线程池提交任务,返回 Future,可以通过 Future 获得任务的执行情况。ScheduledExecutorService
:实现类似于 Timer 的计划任务调度功能。ThreadPoolExecutor
:是 ExecutorService 的一个实现类,也是 Java 中的线程池实现的核心实现类。通常情况下,我们是使用 ThreadPoolExecutor 来实现线程池功能。这些 API 提供以下四种类型的线程池:
Java 中线程池的创建步骤一般为以下三步:
该参数指定核心线程池中线程的数量。当提交一个新任务时,如果当前线程池中的线程数少于 corePoolSize,那么就会创建新的线程。即使其他空闲的非核心线程可以处理新任务,也会继续创建线程,达到核心线程池大小。如果设置为 0,则任务会不断地加入队列,并在工作线程可用时立即执行。
该参数指定总线程池大小,包括核心线程池和非核心线程池。在任务队列满了的情况下,可以创建的最大线程数。如果此时运行的线程数已经等于了 maximumPoolSize,则提交的任务会根据选择的拒绝策略进行处理。
当线程池中的数量大于 corePoolSize 时,这是多余的空闲线程的最长存活时间。直到线程数等于 corePoolSize,超过这个时间,空闲线程就会被回收。
任务队列是存储被提交但尚未被执行的任务的阻塞队列。常用的任务队列有如下几类:
拒绝策略是当任务队列满了需要执行拒绝策略来处理新提交的任务。提供了几种预定义的拒绝策略:
在开始调整线程池之前,需要对当前的系统进行全面的评估。可以通过使用系统监控软件,来确定系统的各项指标如 CPU 使用率、内存使用率、磁盘 I/O 等等,以及确定系统瓶颈。此外还要考虑到系统的上下文切换的开销。
核心线程池大小应该根据需要处理的并发任务数以及 CPU 核心数来确定。通常来说,核心线程数可以设为 CPU 核心数 + 1。
最大线程池大小应该根据系统应对的最高并发数来确定。如果最大线程池大小比较大,会导致系统资源的浪费;如果比较小,会导致请求被拒绝。建议根据硬件资源、负载、并发量等实际情况来确定。
存活时间通常设置为 60s 左右,即当线程池中的线程空闲时间超过了 60 秒,那么这个线程就会被回收。同时,当线程池中的线程数量小于等于核心线程池大小时,存活时间将不起作用。
常用的阻塞队列有 ArrayBlockingQueue 和 LinkedBlockingQueue。
拒绝策略有四种:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。其中,DiscardOldestPolicy 可以尽量取出能够处理的任务,而不是将任务全部丢弃。如果需要响应时间更好,可以使用 CallerRunsPolicy。若任务量极高,建议使用 DiscardPolicy 策略。
核心线程池数量不宜过多,因为每个线程都需要占用内存和 CPU 资源,过多的核心线程池数量会导致系统资源的浪费,从而降低系统性能。但也不应过少,否则会降低系统吞吐量。
最大线程池数量一般设置:最大线程池数量 = CPU 核心数 + 网络连接数 + 其他 IO 等待时间的线程数量
设定一个适当的线程存活时间,可以有效地减少线程的创建和销毁带来的性能开销。在存活时间到达之后,多余的线程会被回收,从而释放系统资源。
任务队列是存储被提交但尚未被执行的任务的阻塞队列。在选择队列类型时,应考虑任务数量和任务类型,以及需要处理的并发请求数。常用的阻塞队列有 ArrayBlockingQueue 和 LinkedBlockingQueue。
拒绝策略通常分为四种:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy 和 DiscardPolicy。根据业务需求和系统负载情况,选择合适的拒绝策略。
在 CPU 密集型应用中,任务主要是 CPU 计算,线程池的大小应该根据 CPU 核心数来设置,以充分利用 CPU 资源,并避免过多线程间的竞争和上下文切换。通常情况下,将核心线程池大小设置为 CPU 核心数,将最大线程池大小设置为 CPU 核心数 * 2。比如,当前服务器有 8 核 CPU,那么推荐设置核心线程池大小为 8,最大线程池大小为 16。
在 IO 密集型应用中,任务主要是从事 IO 等待,而线程的 CPU 计算能力却很小,此时线程数量适当多一点,可以让 CPU 等待 IO 的数目更多,以充分利用计算机的硬件资源。通常情况下,核心线程池大小可以设置为 CPU 核心数 +1,最大线程池大小可以设置为 CPU 核心数 * 2。同时,建议使用无界的 LinkedBlockingQueue 阻塞队列,以避免丢失任务。
对于各种不同的场景,应该根据实际情况进行参数的设置。比如生产者-消费者问题,可以使用 FixedThreadPool,保证消费者线程数量少于核心线程池大小,以确保消费者线程能够及时执行。同时,阻塞队列也可以根据实际情况选择不同的存储方式。对于需要开多个线程处理的应用,可以使用 ScheduledThreadPoolExecutor,定时执行任务。总之,根据实际需求来设置线程池的参数是最重要的。
线程池对于提高系统性能和可管理性非常重要,线程池的性能和效率很大程度上取决于参数的设置。对于不同的业务和场景,需要根据实际情况来设置线程池的参数。
领取专属 10元无门槛券
私享最新 技术干货