目录
前言
线程池概述
线程池的实现
线程池的构造
拒绝策略
任务队列
线程池的工作原理
线程池的监控
Executors线程池工厂
自定义线程池
使用线程池的好处
应用场景
总结
本文详细探讨了线程池在并发编程领域的应用,介绍了ThreadPoolExecutor的核心组件、工作原理,线程池的构造、拒绝策略、任务队列、线程池的监控、线程池工厂等相关内容,并讨论了线程池对系统性能和线程管理的优势。
前言
在面对并发任务处理的场景时,采用多线程策略来并行执行各异的任务成为了提升系统整体执行效率与响应能力的常见手段,然而线程作为操作系统的重要资源,频繁地创建线程不仅会消耗更多的资源,导致频繁的线程上下文切换,加剧共享资源的竞争程度,降低程序的响应速度,还有系统资源耗尽的风险。因此需要对线程的使用进行合理的管理,于是便有了线程池。
线程池概述
线程池的核心功能就是线程的复用,在线程池内,通常会创建一定数量可复用的线程,而不是为每个新任务都单独创建新线程。还有一个阻塞队列,用于保存暂时无法处理的任务,起到缓冲的作用。除此之外,线程池还能定义拒绝策略,当请求的任务太多时,根据拒绝策略抛弃任务,保证已加入线程池的任务可以正常执行完成。这种设计能够显著提高系统资源的使用效率、保证系统的稳定性。
线程池的实现
Java提供了一套Executor框架,帮助开发人员有效地进行线程控制,其本质就是一个线程池,它的核心成员如图所示:
线程池的构造
public ThreadPoolExecutor(int corePoolSizeint maximumPoolSizeLong keepAliveTimeTimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
Java 主要是通过ThreadPoolExecutor类来创建线程池的。
-
corePoolSize:线程池中的核心线程数量。
-
maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
-
keepAliveTime:超出corePoolSize后创建的线程存活时间或者是所有线程最大存活时间,取决于配置。
-
unit:keepAliveTime 的时间单位。
-
workQueue:任务队列,是一个阻塞队列,当线程数达到核心线程数后,会将任务存储在阻塞队列中。
-
threadFactory :线程池内部创建线程所用的工厂,一般用默认即可。
-
handler:拒绝策略,当队列已满并且线程数量达到最大线程数时,会调用该方法处理任务。
拒绝策略
当线程池无法再接收任务时就会根据拒绝策略丢弃任务,JDK自带的拒绝策略实现有4种:
-
AbortPolicy:丢弃任务,抛出运行时异常,这是默认的拒绝策略。
-
CallerRunsPolicy:由提交任务的线程来执行任务。
-
DiscardPolicy:丢弃这个任务,但是不抛异常。
-
DiscardOldestPolicy:从队列中剔除最先进入队列的任务,然后再次提交任务。
如果这些策略无法满足你的需求,你也可以自己实现RejectedExecutionHandler接口自定义拒绝策略,比如快熟返回失败,或者在数据库中记录失败的任务,又或者将任务存在数据库或者缓存中,等到线程池空闲时在取出来执行。
任务队列
-
直接提交的队列:该功能由SynchronousQueue实现。SynchronousQueue是一个特殊的BlockingQueue,它没有容量,提交的任务不会被保存,而总是将新任务提交给线程执行,如果没有空闲的进程,则尝试创建新的进程,如果进程数量已经达到最大值,则执行拒绝策略。
-
有界的任务队列:有界的任务队列可以使用ArrayBlockingQueue类实现。可以设置暂存的最大任务数量。
-
无界的任务队列:无界任务队列一般通过LinkedBlockingQueue类实现。使用无界任务队列当系统的线程数达到corePoolSize后,线程数量就会保持不变。新的任务会加入任务队列。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。
-
优先任务队列:优先任务队列是带有执行优先级的队列。它通过PriorityBlockingQueue类实现,可以控制任务的执行先后顺序。
线程池的工作原理
-
线程池创建:在程序启动时或者首次需要时,预先创建一定数量的线程,并将这些线程放入线程池中,等待任务分配。
-
任务提交:当有新的任务到来时,不是直接创建新线程去执行任务,而是将任务添加到任务队列中。
-
任务调度与执行:线程池中的空闲线程会从任务队列中取出任务并执行。如果所有线程都在忙碌状态且任务队列已满,根据策略可能拒绝新任务,或者挂起直到有线程可用。
-
线程复用:任务执行完毕后,线程不会立即销毁,而是返回线程池等待下一次任务分配,这大大减少了创建和销毁线程的开销。
-
动态调整:线程池可以根据预设的策略动态调整线程数量,比如在任务量激增时增加线程数以应对高负载,或在任务较少时减少线程数以节省资源。
如图所示为线程池的核心执行流程。当任务被提交至线程池时,首先会尝试创建核心线程以执行这些任务。一旦核心线程数量达到预设的最大值,后续任务将会被转入阻塞队列进行排队等待。在阻塞队列达到其容量上限之后,线程池将继而创建非核心线程来进一步处理任务,直至线程总数触及最大限定值。在此之后,根据所配置的拒绝策略,超出承载能力的任务将被适当处理,例如直接丢弃。值得一提的是,线程池允许用户自定义任务拒绝策略,以实现更为灵活的错误处理机制。一种进阶策略可能是将无法立即执行的任务暂存于数据库或缓存系统中,这样一来,即使任务最初被线程池拒绝,依然能够通过检索数据库或缓存来恢复这些任务并考虑后续的重试或其他补偿处理措施。
线程池的监控
对线程池的监控可以帮助我们了解任务的执行状况,方便出问题的时候快速定位,线程池提供了一些方法来获取线程池的运行状态。
-
getCompletedTaskCount():获取已经执行完成的任务数量
-
getLargestPoolSize():获取线程池里曾经创建过的最大的线程数量。
-
getActiveCount():获取正在执行任务的线程数据。
-
getPoolSize():获取当前线程池中线程数量的大小。
-
getTaskCount():返回线程池已接收但尚未完成(包括正在执行和排队等待)的任务总数。
线程池内置的方法也许并不能满足实际需求,可以自己继承ThreadPoolExecutor重写相关方法实现自定义的监控策略,也可以通过引入一些第三方组件来支持更加强大的功能。
Executors线程池工厂
Executors是一个线程池工厂类,提供了创建不同类型线程池的方法,使得创建线程池变得十分简单方便。
-
newFixedThreadPool创建一个线程数量固定的线程池,它的线程数量保持固定,即使没有作业也不会关闭线程。内部使用了一个无界队列LinkedBlockingQueue作为阻塞队列。
-
newCachedThreadPool创建一个缓冲线程池,它的等待队列不缓存任务,当有任务提交时使用空闲线程执行,如果没有空闲线程则创建新线程执行,空闲线程超过60秒没被执行则会被关闭。所以它适合短周期任务,长周期任务可能会创建过多的线程。
-
newSingleThreadExecutor这个线程池只有一个线程,这个线程池可以在线程死后重新启动一个线程来替代原来的线程继续执行下去。
-
newScheduledThreadPool带定时调度功能的线程池。
经常看到一些文章指出不推荐使用Executors创建线程池,因为这些线程池都没有任务的数量或者是线程的数量,可能会导致系统资源耗尽。我个人的看法是,在复杂的场景中确实不推荐使用,但是在简单的、执行过程可预知的一些任务场景中还是没问题的,因为它简洁方便,代码可读性好,能让我早点下班。
自定义线程池
通过 Executors 这个工具类来创建的线程池如果无法满足实际的使用场景,那么在实际的项目中,该如何合理地构造线程池呢?其中线程数的设置主要取决于业务 IO 密集型还是 CPU 密集型。除线程数的设置外,其它的参数设置也需要根据实际需求进行判断。
-
CPU 密集型:指的是任务主要使用来进行大量的计算,没有什么导致线程阻塞,这种场景设置太多的线程,导致线程频繁的上下文切换,反而会降地程序执行的速度,一般线程数设置为CPU核心数。
-
IO 密集型:当执行任务需要大量的io,比如磁盘io,网络io,可能会存在大量的阻塞,阻塞的过程中线程可以执行其它的任务,所以在IO密集型任务中使用多线程可以大大地加速任务的处理。一般线程数设置为2*CPU核心数。
使用线程池的好处
-
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度:当任务到达时,可以不需要等到线程创建就能立即执行,提高了响应速度。
-
线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
-
程序的健壮性:线程若意外挂掉,线程池会重新创建新线程继续执行,某个任务异常不会影响其它任务执行。
应用场景
线程池技术的应用范围十分广泛,如Web服务器请求处理、数据库连接管理、异步任务执行、定时任务调度、并发性能测试及批量数据处理等诸多领域。它在面对那些具有短期性、高吞吐量以及突发性特点的任务处理需求时,展现了尤为显著的优势。通过预创建和管理线程资源,线程池能有效提升系统对高并发场景的响应速度与处理能力,同时优化资源利用效率,确保系统的稳定性和可靠性。
总结
不同的编程语言提供了不同级别的线程池实现框架,如Java中的ExecutorService
接口及其实现类ThreadPoolExecutor
。开发者可以根据具体需求配置线程池的核心参数,如核心线程数、最大线程数、任务队列大小、饱和策略等,以达到最佳的性能和资源利用率。总之,线程池作为一种高效管理线程资源的方式,在现代软件开发中扮演着至关重要的角色,特别是在构建高性能、高并发系统时。正确理解和应用线程池,对于提升系统的稳定性和效率具有重要意义。