动态线程池组件-线程池实现原理

2,233 阅读7分钟

1.线程池简介

1.1 线程池是什么?

       线程池是基于池化技术来管理线程的工具,经常出现在多线程服务器中

线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

使用线程池有以下好处:

降低资源消耗: 通过池化技术重复利用已有的线程,降低线程创建与销毁的损耗。

提高相应速度:任务到达时,无需等待创建好线程即可立刻执行。

提高线程的可管理性:线程是有限资源,(JVM启动时可指定ThreadStackSize大小)如无限制创建,不仅会消耗系统资源,而且还会造成系统资源不合理分配,影响应用性能。使用线程池可以进行统一的分配、调优和监控;

提供更强大的扩展: 线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

1.2 线程池解决的问题是什么?

线程池解决的核心问题是资源管理,在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性带来以下若干问题:

a.频繁申请/销毁资源和调度资源,将带来额外的消耗;

b.对资源无限的申请缺少管控手段,可能会耗尽资源;

c.系统无法合理地管控资源,会降低系统的稳定性.

1.3 线程池的核心设计与实现

1.3.1 总体设计

Java中的核心线程池是 ThreadPoolExecutor,从右往左是继承关系。

Executor <- ExecutorService <- AbstractExecutorService <- ThreadPoolExecutor

Executor的设计思想是:将任务提交和任务执行解耦,用户无需关注如何创建线程,用户只需要提供一个runnable对象, 将任务的运行逻辑提交到Executor(执行器)中, 由Executor完成资源的调配和任务的执行。

ExecutorService扩展了一些能力:扩充执行任务的能力,补充一个或一批异步任务生成future的方法,提供了管控线程的方法,比如submit/invoke/shutdown;

AbstractExecutorService: 上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法;

ThreadPoolExecutor:将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。其中运行机制如下图所示

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,从而形成良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:

(1)直接申请线程执行该任务;

(2)缓冲到队列中等待线程执行;

(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

1.3.2 线程池生命周期

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起,如下代码所示:

ctrl这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。

关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如以下代码所示:

目前线程池有5种生命周期状态,如下图所示。

1.3.3 任务执行机制

任务调度

首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

a.首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

b.如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

c.如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

d.如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

任务缓冲

任务缓冲模块是线程池能够管理任务的核心,线程池本质是对任务和线程的管理,关键是将线程和任务解耦,不直接关联,线程池通过以消费者-生产者模式来实现,阻塞缓冲队列缓存任务, 工作线程则是从阻塞队列中获取任务来执行。

 BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。常用的几种BlockingQueue:

  • ArrayBlockingQueue:规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。

  • LinkedBlockingQueue:大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。

  • PriorityBlockingQueue:类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。

  • SynchronizedQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成。

任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。拒绝策略是一个接口,可通过实现接自定义的拒绝策略执行器

2.线程池的实践

2.1 业务背景

在当今的互联网业界,为了最大程度利用CPU的多核性能,并行运算的能力是不可或缺的。通过线程池管理线程获取并发性是一个非常基础的操作,让我们来看两个典型的使用线程池获取并发性的场景。

场景1: 快速处理用户需求

描述: 在我们的课表业务中,在用户请求课表信息需要抓取大量的数据组装起来返回给用户

分析:在该业务场景下,我们希望是越快响应用户需求越好,但由于课表信息非常复杂,伴随着调用与调用之间的级联、多级级联等情况,开发童鞋通常使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。该场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

场景2:

描述:离线的大量计算任务,需要快速执行。在我们的打卡业务中,需要生成学校下以年级、班级、老师、家长等各个维度的统计数据,如xx班级,打卡/未打卡人数。

批量处理任务

分析:这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

2.2实际问题以及思考方案

2.2.1 实际问题及方案思考

线程池使用面临的核心的问题在于:线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。

尽管经过谨慎的评估,仍然不能够保证一次计算出来合适的参数,那么我们是否可以将修改线程池参数的成本降下来,这样至少可以发生故障的时候可以快速调整从而缩短故障恢复的时间呢?基于这个思考,我们是否可以将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化前后的参数修改流程对比如下:

各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是大神👍 每篇章内容不能保证完全原创,但也经过深思熟虑与实践的思想结晶,技术文章相互参考借鉴是经常的,如有版权问题,先跟作者致歉,目前还未用于商业用途,如有会主动联系,发现问题请大家马上联系留言。

非常感谢大神们能看到这里,如果这个文章写得还不错,觉得有点东西的话

求点赞👍 求关注❤️ 求分享👥