高并发中,那些不得不说的线程池与ThreadPoolExecutor类
摘要:从整体上认识下线程池中最核心的类之一——ThreadPoolExecutor,关于ThreadPoolExecutor的底层原理和源码实现,以及线程池中的其他技术细节的底层原理和源码实现。
本文分享自华为云社区《高并发之——不得不说的线程池与ThreadPoolExecutor类浅析》,作者: 冰 河 。
既然Java中支持以多线程的方式来执行相应的任务,但为什么在JDK1.5中又提供了线程池技术呢?这个问题大家自行脑补,多动脑,肯定没坏处,哈哈哈。。。
说起中的线程池技术,在很多框架和异步处理中间件中都有涉及,而且性能经受起了长久的考验。可以这样说,Java的线程池技术是Java最核心的技术之一,在Java的高并发领域中,Java的线程池技术是一个永远绕不开的话题。既然Java的线程池技术这么重要(怎么能说是这么重要呢?那是相当的重要,那家伙老重要了,哈哈哈),那么,本文我们就来简单的说下线程池与ThreadPoolExecutor类。
一、Thread直接创建线程的弊端
(1)每次new Thread新建对象,性能差。
(2)线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM。
(3)缺少更多的功能,如更多执行、定期执行、线程中断。
(4)其他弊端,大家自行脑补,多动脑,没坏处,哈哈。
二、线程池的好处
(1)重用存在的线程,减少对象创建、消亡的开销,性能佳。
(2)可以有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。
(3)提供定时执行、定期执行、单线程、并发数控制等功能。
(4)提供支持线程池监控的方法,可对线程池的资源进行实时监控。
(5)其他好处,大家自行脑补,多动脑,没坏处,哈哈。
三、线程池
1.线程池类结构关系
线程池中的一些接口和类的结构关系如下图所示。
后文会死磕这些接口和类的底层原理和源码。
2.创建线程池常用的类——Executors
- Executors.newCachedThreadPool:创建一个可缓存的线程池,如果线程池的大小超过了需要,可以灵活回收空闲线程,如果没有可回收线程,则新建线程
- Executors.newFixedThreadPool:创建一个定长的线程池,可以控制线程的最大并发数,超出的线程会在队列中等待
- Executors.newScheduledThreadPool:创建一个定长的线程池,支持定时、周期性的任务执行
- Executors.newSingleThreadExecutor: 创建一个单线程化的线程池,使用一个唯一的工作线程执行任务,保证所有任务按照指定顺序(先入先出或者优先级)执行
- Executors.newSingleThreadScheduledExecutor:创建一个单线程化的线程池,支持定时、周期性的任务执行
- Executors.newWorkStealingPool:创建一个具有并行级别的work-stealing线程池
3.线程池实例的几种状态
- Running:运行状态,能接收新提交的任务,并且也能处理阻塞队列中的任务
- Shutdown: 关闭状态,不能再接收新提交的任务,但是可以处理阻塞队列中已经保存的任务,当线程池处于Running状态时,调用shutdown()方法会使线程池进入该状态
- Stop: 不能接收新任务,也不能处理阻塞队列中已经保存的任务,会中断正在处理任务的线程,如果线程池处于Running或Shutdown状态,调用shutdownNow()方法,会使线程池进入该状态
- Tidying: 如果所有的任务都已经终止,有效线程数为0(阻塞队列为空,线程池中的工作线程数量为0),线程池就会进入该状态。
- Terminated: 处于Tidying状态的线程池调用terminated()方法,会使用线程池进入该状态
注意:不需要对线程池的状态做特殊的处理,线程池的状态是线程池内部根据方法自行定义和处理的。
4.合理配置线程的一些建议
(1)CPU密集型任务,就需要尽量压榨CPU,参考值可以设置为NCPU+1(CPU的数量加1)。
(2)IO密集型任务,参考值可以设置为2*NCPU(CPU数量乘以2)
四、线程池最核心的类之一——ThreadPoolExecutor
1.构造方法
ThreadPoolExecutor参数最多的构造方法如下: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler rejectHandler)
其他的构造方法都是调用的这个构造方法来实例化对象,可以说,我们直接分析这个方法之后,其他的构造方法我们也明白是怎么回事了!接下来,就对此构造方法进行详细的分析。
注意:为了更加深入的分析ThreadPoolExecutor类的构造方法,会适当调整参数的顺序进行解析,以便于大家更能深入的理解ThreadPoolExecutor构造方法中每个参数的作用。
上述构造方法接收如下参数进行初始化:
(1)corePoolSize:核心线程数量。
(2)maximumPoolSize:最大线程数。
(3)workQueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响。
其中,上述三个参数的关系如下所示:
- 如果运行的线程数小于corePoolSize,直接创建新线程处理任务,即使线程池中的其他线程是空闲的。
- 如果运行的线程数大于等于corePoolSize,并且小于maximumPoolSize,此时,只有当workQueue满时,才会创建新的线程处理任务。
- 如果设置的corePoolSize与maximumPoolSize相同,那么创建的线程池大小是固定的,此时,如果有新任务提交,并且workQueue没有满时,就把请求放入到workQueue中,等待空闲的线程,从workQueue中取出任务进行处理。
- 如果运行的线程数量大于maximumPoolSize,同时,workQueue已经满了,会通过拒绝策略参数rejectHandler来指定处理策略。
根据上述三个参数的配置,线程池会对任务进行如下处理方式:
当提交一个新的任务到线程池时,线程池会根据当前线程池中正在运行的线程数量来决定该任务的处理方式。处理方式总共有三种:直接切换、使用无限队列、使用有界队列。
- 直接切换常用的队列就是SynchronousQueue。
- 使用无限队列就是使用基于链表的队列,比如:LinkedBlockingQueue,如果使用这种方式,线程池中创建的最大线程数就是corePoolSize,此时maximumPoolSize不会起作用。当线程池中所有的核心线程都是运行状态时,提交新任务,就会放入等待队列中。
- 使用有界队列使用的是ArrayBlockingQueue,使用这种方式可以将线程池的最大线程数量限制为maximumPoolSize,可以降低资源的消耗。但是,这种方式使得线程池对线程的调度更困难,因为线程池和队列的容量都是有限的了。
根据上面三个参数,我们可以简单得出如何降低系统资源消耗的一些措施:
- 如果想降低系统资源的消耗,包括CPU使用率,操作系统资源的消耗,上下文环境切换的开销等,可以设置一个较大的队列容量和较小的线程池容量。这样,会降低线程处理任务的吞吐量。
- 如果提交的任务经常发生阻塞,可以考虑调用设置最大线程数的方法,重新设置线程池最大线程数。如果队列的容量设置的较小,通常需要将线程池的容量设置的大一些,这样,CPU的使用率会高些。如果线程池的容量设置的过大,并发量就会增加,则需要考虑线程调度的问题,反而可能会降低处理任务的吞吐量。
接下来,我们继续看ThreadPoolExecutor的构造方法的参数。
(4)keepAliveTime:线程没有任务执行时最多保持多久时间终止
当线程池中的线程数量大于corePoolSize时,如果此时没有新的任务提交,核心线程外的线程不会立即销毁,需要等待,直到等待的时间超过了keepAliveTime就会终止。
(5)unit:keepAliveTime的时间单位
(6)threadFactory:线程工厂,用来创建线程
默认会提供一个默认的工厂来创建线程,当使用默认的工厂来创建线程时,会使新创建的线程具有相同的优先级,并且是非守护的线程,同时也设置了线程的名称
(7)rejectHandler:拒绝处理任务时的策略
如果workQueue阻塞队列满了,并且没有空闲的线程池,此时,继续提交任务,需要采取一种策略来处理这个任务。
线程池总共提供了四种策略:
- 直接抛出异常,这也是默认的策略。实现类为AbortPolicy。
- 用调用者所在的线程来执行任务。实现类为CallerRunsPolicy。
- 丢弃队列中最靠前的任务并执行当前任务。实现类为DiscardOldestPolicy。
- 直接丢弃当前任务。实现类为DiscardPolicy。
2.ThreadPoolExecutor提供的启动和停止任务的方法
(1)execute():提交任务,交给线程池执行
(2)submit():提交任务,能够返回执行结果 execute+Future
(3)shutdown():关闭线程池,等待任务都执行完
(4)shutdownNow():立即关闭线程池,不等待任务执行完
3.ThreadPoolExecutor提供的适用于监控的方法
(1)getTaskCount():线程池已执行和未执行的任务总数
(2)getCompletedTaskCount():已完成的任务数量
(3)getPoolSize():线程池当前的线程数量
(4)getCorePoolSize():线程池核心线程数
(5)getActiveCount():当前线程池中正在执行任务的线程数量

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
QQ浏览器信息流云原生应用之路
背景 QQ 浏览器信息流(QB)推荐架构支撑了 QQ 浏览器、快报主 feeds 场景、浮层等信息流卡片实时推荐的能力,架构上不仅仅要支持多业务、多产品,如 QB 、快报、外部合作等,而且需要能够快速支持各种类型场景的能力,如主 TL 、浮层,且能够快速扩展支持垂直频道和 APP 。那么信息流推荐架构需要做到灵活模块化,水平易扩展。 为了做到海量级实时精准推荐,信息流推荐架构划分为了四层:展控层、排序层(精排/粗排)、召回层、索引层,并提供实时级用户画像模型打分模块和特征系统,进行实时的用户/物品特征累积,实时反馈给推荐链路进行千万级索引文章/视频的筛选和精准推荐。具体的架构模块图如下所示: 可以看到,推荐架构容纳的模块之多,支持的业务形式之多,而且需要支撑亿级用户规模和百亿特征规模,那么需要更好的技术架构体系对于如此庞大的架构服务和存储去进行开发、扩展、管理、维护等。 挑战 挑战一:实时性 (1)特征实时更新 (2)模型在线实时学习 挑战二:超大规模 (1)用户量级:亿级用户 (2)特征规模大:百亿特征、千亿参数(无量) (3)样本庞大:精排样本每日样本近百TB 挑战三...
- 下一篇
给Arm生态添把火,腾讯Kona JDK Arm架构优化实践
Arm架构以其兼具性能与功耗的特点,在智能终端以及嵌入式领域得到了广泛的使用,不断扩大其影响力。而在PC端以及数据中心,之前往往是x86架构在其中发挥着主要的作用。最近,随着人工智能、云计算等技术的兴起,5G网络的不断成熟,万物互联的时代是的应用的需求越来越多样化,使得对于芯片架构的需求也越来越多样化。 Arm架构在提供可靠性能的基础上,低功耗、低开销的特点使得它被越来越广泛的应用到数据中心和云计算中,成为其中必不可缺少的重要组成部分。亚马逊投入大量精力自研Arm服务器,并应用到AWS服务中,最多实现了成本45%的降低;阿里巴巴也在云服务中大量采用Arm服务器,并积极参与Linaro,Adoptium等组织,不断推动Arm架构的发展。 最近几年,腾讯对于Arm架构的需求也不断增加,各个产品线也不断引入Arm服务器,对于Arm架构软件的需求也在不断增长。KonaJDK团队在腾讯公司内部提供高性能、高稳定性的商用JDK版本,坚定地将Arm架构作为KonaJDK重点支持的架构之一,不断扩展JDK在Arm架构的功能,并不断提高Arm架构中JDK的性能。 随着Arm架构在终端和云计算场景的广泛应...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS关闭SELinux安全模块
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Docker使用Oracle官方镜像安装(12C,18C,19C)