在云计算、5G技术快速发展的互联网世界,为了快速响应用户的请求,宏观上除了团队内部实行DevOps机制管理、使用微服务架构进行技术设计、使用Docker或K8s进行应用部署外,微观上在程序开发中使用并行计算的能力也是必不可少的。
而在Java开发中,最常用的便是通过线程池来最大程度利用CPU资源,实现多任务并行。
我们先来看一个用户请求快速响应的案例:北京在五一假期前的突然将应急响应程度从一级降为二级,从低风险地区入京不需要隔离,这消息一经放出,仿佛沉寂的北京和人们又活过来了。
大家纷纷开始在各平台购买机票、火车票,试想当你在去哪儿网查询从北京到日本的机票时,半天都刷不出来,又或是先有航班的班次、再有价格、继而有座位出来、最后出来整个图片(串行执行),蜗牛般的速度让你瞬间就离开该平台了。
为了快速的响应用户请求,在程序开发中一般采用多线程并发执行,即当用户发起查询航班请求时,将获取航班班次、价格信息、座位信息、图片信息这四个任务一起执行(并行执行),再返回给用户,将原来的时间缩减3/4。
在本案例中通过多线程并发执行的方式快速的响应了用户请求,接下来我们介绍线程池~
在介绍线程池原理之前,首先得了解什么是线程池。线程池,望文生义,就是线程的池子,里面有很多很多的线程。
我们知道一个程序运行时是一个进程,而程序里有很多的方法要去执行,每个方法就是一个线程,在刚刚的案例中去哪儿平台程序就是一个进程,里面获取航班班次的函数、获取航班价格的函数、获取航班位置的函数就是多个线程。
每个函数在运行时,都需要先把线程创建起来,然后运行,最后函数执行完毕销毁线程。如果每个函数运行时都去创建线程、运行完毕都去销毁线程,这实现太耗费线程资源,如果有一个地方专门负责线程的创建和销毁,程序的函数要运行时直接去申请,那么资源的消耗是不是就降低了很多(不需要创建和销毁)、函数的响应速度是不是就提高了很多呢?(每次来就使用了,不需要去创建)、线程的管理是不是就更专业了呢?(有专门的地方管理线程),是的,这个地方就是线程池,通过池化的思想统一管理分配线程。
接下来我们介绍在Java中线程池是如何实现的。Java中的线程池核心实现包括四个模块Executor、ExecutorService、AbstractExecutorService、ThreadPoolExecutor。
Executor是线程池对外的接口,研发人员只需将需要运行的函数(即任务)传递给Executor即可,Executor就会完成线程的调配和任务的执行部分。
ExecutorService是对Executor能力的扩展,研发人员是将任务一个个的传递给Executor,但是ExecutorService可将多个任务提炼成一个总任务,并且可管控线程池。
AbstractExecutorService是对上层的抽象,将执行任务的流程串联起来,使得最底层ThreadPoolExecutor只关注于任务的实现即可。ThreadPoolExecutor则是最复杂的底层,一方面要维护自身生命周期,一方面管理线程和任务。
那么ThreadPoolExecutor是如何管理线程和任务呢?
其中在它内部也维护着一个生产者消费者模型,在介绍消息中间件MQ的时候我们也详细地介绍过生产者消费者,它的优点之一是实现了解耦,即生产者往队列里发送任务,不必等待该任务执行完再发送下一个生产者,消费者只管从队列里获取任务进行线程分配,不必等到生产者发送任务。
在ThreadPoolExecutor中任务管理便是生产者,线程管理便是消费者,当任务提交后,线程池判断该任务得如何执行。
在线程池内部有五种状态,Running则表示该线程能接受新提交的任务并且也能处理阻塞队列中的任务。Shutdown则表示不能接受新提交的任务但可以继续处理阻塞队列中已保存的任务。Stop则表示不能接受新任务,也不能处理队列中的任务,会中断正在处理任务的线程。Tidying则表示所有的任务都终止了,有效线程数为0;Terminated则表示终结状态。其生命周期的转化如图所示。
当任务进来时,线程池首先会检查自己的状态,如果不是Running状态,那么直接拒绝任务的执行;如果线程是Running状态,而且线程数量<线程池正常大小数(即没有任务需要执行时线程池的大小,简称核心数corePoolSize),那么创建并启动一个线程来执行新提交的任务;如果线程数量>;核心数,并且线程池内的阻塞队列没有满,那么将该任务加入到阻塞队列等待执行;如果线程数量>;核心数并且<线程池最大数,并且线程池内的阻塞队列没有满,那么创建一个新的线程来执行提交的任务,如果线程数量>线程池最大线程数,并且线程池内的阻塞队列已满,那么拒绝处理该任务。
因此在线程池管理中,最大线程数、线程池正常大小数非常重要,如果过少可能导致线程不够用,任务不能执行,如果过多可能导致任务在缓存队列里等待时间长,最终超时不能执行。对于该数量的设置,目前也没有官方的算法,更多是通过监控数据和业务运行特征来不断地调整。
通过线程池统一管理线程能提高资源的使用率、提高用户响应时间。事实上,在程序世界里,除了运行函数的线程使用了池化管理的方式之外,当程序连接数据库时,也通过数据库连接池的方式统一管理数据库连接资源,当程序运行需要内存时,也通过内存池的方式统一管理内存资源。
这种统一化管理资源的方式,使得用户在低投入中获取了最高效率的资源利用,实现了共赢。
这就和链接、我爱我家、自如这样的大型房地产公司统一管理出租房源是一样的道理。以前租客要租房屋时,需要找到多个房东,咨询详细地理位置、价格、房屋图片,货比三家后再进行签约。而房屋中介将房屋收置后,租客要租房屋只需要提交自己的租房要求(地理位置&价格),中介就会对应的提供很多选择,并且推荐最合适的给你。通过统一化管理的方式提高了租客的租房效率,实现了共赢。
在互联网快速发展的今天,任何一家企业想要长久的站稳市场,除了提供的产品能满足用户不断变化的需求之外,产品的好用性能也是非常重要的,通过多线程开发的模式能很好的提高程序性能,本文只是抛砖引玉介绍了Java线程池的使用场景、实现原理、解决问题,但如何让其服务于良好的产品性能,就需要大家在实践中不断地摸索总结了~
添加VX13125006136进行云计算,JAVA,大数据学习在线咨询,获取更多免费学习资料