目录
一、简介
在平时的开发中,系统的性能和吞吐量是衡量系统的重要指标,那提高系统性能和吞吐量的方式你知道有哪些吗?如果单从技术的角度考虑,不考虑业务流程的优化,那优化的方式还是很多的,比如:
- 减少网络通信:比如能批量通过接口查询的尽量避免多次循环查询
- 减少 IO 操作:比如操作数据库时能批量操作的选择批量操作,减少与数据库的操作
- 使用缓存:将数据缓存起来,减少网络请求等操作
- 数据库建立合适的索引的
- 减少锁的所用
- 使用分布式设计
- 异步编程
- 使用多线程
以上列举了一部分,还有其他一些优化技术就不一一列举了,本文的主角是动态线程池,下面详细介绍一下多线程编程。
二、多线程
首先来思考下为什么使用多线程技术呢?
关于这方面的例子就不详细介绍了,相信你已经很清楚了。使用多线程的技术的主要目的是充分利用计算机的计算处理能力,提升程序的处理能力,进而增强程序的响应性和稳定性,带来更好的用户体验。
而且在平时的编程中,如果使用多线程技术的话,一定会使用线程池,那为什么会使用线程池呢?下面先来看下线程的实现方式。
三、线程实现方式
线程的实现有两种方式,分别为内核线程实现和用户线程实现,下面分别介绍下。
3.1 内核线程实现
使用内核线程实现的方式称为 1:1 实现。内核线程(Kernel-Level Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就称为内核线程。
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process, LWP),轻量级进程就是我们通常意义上讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级线程。这种轻量级进程与内核线程之间1:1的关系成为一对一线程模型。
由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中一个轻量级进程被阻塞,也不会影响整个进程继续工作。轻量级进程也具有极限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Model)中来回切换。其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源,因此一个系统支持轻量级进程的数量是有限的。
3.2 用户线程实现
使用用户线程实现的方式被称为1:N实现。广义上讲,一个线程只要不是内核线程,都可以认为是用户线程(User Thread, UT)的一种,因此轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,因此效率会受到限制,并不具备通常意义上的用户线程的优点。用户线程的创建、同步、销毁和调度完全在用户中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换内核态,因此操作可以是非常快速且低消耗的,也能够支持更大规模的线程数量。部分高性能数据库的线程就是由用户线程实现的。这种用户线程之间的1:N的关系成为一对多的线程模型。
用户线程的优势在于不需要系统内核支援,劣势在于没有系统的支援,所有的线程操作都需要用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题。
在 Java 中线程的实现采用的是内核线程实现的方式,因为每次创建或销毁的操作都需要进行系统调用,所以它的代价相对较高,这时就出现了线程池技术,避免了频繁的创建或销毁线程带来的性能开销。
四、线程池
线程是稀缺资源,它的创建销毁是一个相对偏重的操作,而 Java 采用 1:1 模型通过内核实现线程,创建线程需要进行系统切换,为了避免过度消耗,需要设法复用线程,而线程池就是线程的缓存,负责对线程进行统一的分配与管理。
创建线程池的方式通常是通过 ThreadPoolExecutor 来创建,源码如下:
ThreadPoolExecutor(int corePoolSize,// 核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//空闲线程存活时间
TimeUnit unit,//存活时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory, // 线程工厂,扩展用
RejectedExecutionHandler handler)//拒绝策略
当 ThreadPoolExecutor 被创建时,里边是没有工作线程的,直到有任务进来才开始创建线程去工作,工作原理如下:
当调用线程的 execut 方法时,如果当前的工作线程小于核心线程数,则创建新的线程执行任务;负责将任务加入到阻塞队列中,如果队列满了,则根据最大线程数去创建额外的工作线程去执行任务;如果工作线程达到最大线程数,则根据拒绝策略去执行。存活时间到期的话只是回收核心线程以外的线程。
线程池核心参数介绍:
- corePoolSize:核心线程数,在创建线程池时,默认情况下线程池中没有任何线程,而是等到任务到来才创建线程去执行任务,除非调用了 prestartAllCoreThreads() 或 prestartCoreThread() 方法,这两个方法时预创建线程的意思,即在没有任务到达前就创建 corePoolSize 个核心线程。默认情况下是不创建的,当线程数达到了 corePoolSize 后,就会把任务放入阻塞队列中缓存起来。
- maxiumPoolSize:线程池的最大线程数,这个参数很重要,表示在线程池中最多能创建多少个线程。
- keepAliveTime:表示线程没有任务执行时的存活时间。默认情况下,只有当线程池中的线程大于 corePoolSize 时这个参数才会起作用,即核心线程数之外的线程的存活时间。如果调用了 allowCoreThreadTimeOut(boolean) 方法,在线程池中的线程数不大于 corePoolSize 时,该参数也会起作用。
- unit:参数 keepAliveTime 的时间单位,如:TimeUnit.SECONDS。
- workQueue:阻塞队列,用来存储等待执行的任务,这个参数的选择很重要,会对线程池的运行产生重大影响,因为他直接影响到线程池的行为、性能和资源管理
- 拒绝策略:拒绝策略指当线程池无法接受新任务时所采取的一系列措施。当线程池中的线程都在忙,并且阻塞队列已经满了的时候,线程池就需要决定如何处理新提交的任务。线程池中提供了四种拒绝策略,分别为:
- AbortPolicy:线程池中的线程和队列都满了,默认执行该策略,直接丢弃任务并抛出 RejectedExecutionException 异常;
- DiscardPolicy:线程池中的线程和队列都满了的情况下丢弃任务,但是不抛出异常
- DiscardOldestPolicy:将最早进入队列的任务删除,之后再尝试提交任务
- CallerRunsPolicy:当提交新任务时,如果线程池已满且工作队列已满,则该任务会在调用者的线程中直接执行。
五、动态线程池
通过以上内容,相信你已经非常了解线程池了,通常线程池的参数都要经过合理的评估才能确定,但可能仍然不能满足实际的场景,如果要调整参数,这时需要修改参数并重新部署服务,这时就需要使用动态线程池了。
动态线程池的核心目标:
- 简化线程池参数:线程池的核心参数有三个,分别为 corePoolSize、maxiumPoolSize、workQueue,他们最大程度的决定了线程池的任务分配和线程分配策略。
- 参数动态修改:为了解决参数修改成本高的问题,可以将参数放置在配置平台,如Nacos,可以根据需要动态调整线程池参数。
那哪些参数可以调整呢?通过查看源码可知有如下参数:
JDK 允许线程池使用方通过 ThreadPoolExecutor 的实例来动态设置线程池的核心策略,以setCorePoolSize为 方法例,在运行期线程池使用方调用 setCorePoolSize 方法后,线程池会直接覆盖原来的 corePoolSize 值,并且基于当前值和原始值的比较结果采取不同的处理策略。
- 对于当前值小于当前工作线程数的情况,说明有多余的 worker 线程,此时会向当前 idle 的worker 线程发起中断请求以实现回收,多余的 worker 在下次 idel 的时候也会被回收;
- 对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的 worker 线程来执行队列任务。
原生的阻塞队列由于其容量(capacity)都被final修饰,不可被修改,所以我们需要自行实现一个阻塞队列,为其添加get,set方法,为了保证线程安全需要添加volatile,实现方式也很简单。
五、总结
动态线程池的重要性在于其能够根据实时的工作负载动态调整线程的数量,从而优化资源使用,提高系统性能,并增强系统的稳定性和响应性。
往期经典推荐:
Spring Events 详解:解锁事件驱动架构-CSDN博客
Logback 日志打印导致程序崩溃的实战分析_logback 打日志导致卡死-CSDN博客
Sentinel与Nacos强强联合,构建微服务稳定性基石的重要实践_nacos sentinel-CSDN博客
SpringBoot项目并发处理大揭秘,你知道它到底能应对多少请求洪峰?_一个springboot能支持多少并发-CSDN博客