背景介绍大家好,今天给大家讲一个比较硬核的技术知识,就是Java线程池在生产项目中的高并发优化。可能很多兄弟都听说过Java线程池的理论原理,也知道它是如何工作的,但是却从来没有在项目中玩过Java线程池,更谈不上Java线程池在高并发环境下的优化了。那么今天就来讨论下这个Java线程池在生产项目中的高并发优化吧!线程池的基本工作原理既然要说线程池,至少大家应该对Java线程池的基本工作原理有所了解。我必须单独写一篇文章。这不是我们这次的主题,所以先告诉大家线程池最简单的原理。线程池,简单来说就是有一个池子,里面有一堆线程。一般这些线程不会被销毁,它们会一直存在,然后就可以不断的向线程池提交任务。线程池会取出线程来执行你的任务。任务执行完后,线程不会终止,继续在线程池中待命。我们看下图1:但是这时候有一个关键的问题,就是线程池中的线程数通常是有限制的。注意,这里说的是通常情况,因为Java线程池的真正原理,其实通过定制的手段,Java线程池可以有各种各样的表现。我们这里说的是最基本的情况。即线程池中的线程数量是固定的、有限的。那么如果你一次性向线程池提交了太多的任务,而此时所有的线程都在忙着运行自己的任务,如果这个时候你要提交新的任务,你认为会发生什么情况呢?可以提交任务吗?看下图2:当然不能提交了,但是这个时候线程池只能拒绝你吗?这不是真的。为了应对这种情况,线程池通常会设置一个队列让你提交任务,让你的任务在队列中等待一段时间。当一个线程完成自己的任务并变得空闲时,再次运行该线程。队列中的任务。注意,这也是通常的情况,因为Java线程池其实可以通过定制有其他的表现,但是通常我们会这样设置线程池。如下图3所示:分析完线程池高并发场景下的问题,那么接下来的问题来了。以上就是Java线程池最基本的原理和用法,但是真正投入到生产项目中之后,他会遇到什么样的问题呢?首先,最大的问题是提交到线程池的任务可能都需要执行各种网络IO任务。比如RPC调用其他服务,或者在后台处理DB中的大量数据,那么一个线程运行完一个任务可能需要很长时间,少则几百毫秒,多则几秒,甚至几十秒。有这样的可能。如下图4:第二个问题,大家注意到上图中没有这个东西,就是有的任务是RPC调用,可能只需要几百ms,有的任务是大数据量操作,这可能需要几十秒。所以,其实在一个普通的线程池中,各种任务都在运行,这就导致线程池中的一个线程什么时候能完成一个任务是不确定的,因为这个任务可能是一个RPC调用,也可能是一个大数据量加工。第三个问题,一个Http请求中可能会有一些任务。本来,在一个Http请求的处理过程中,会依次处理多个耗时的任务。现在为了优化性能,需要将多个任务提交到线程池中,使用多个线程并发执行多个任务来提高本次请求的性能。这个Http请求需要等待这多个并发运行的任务执行完毕才会返回响应给用户。如下图5所示:那么,最终的大问题就是生产项目中运行的这种线程池是提供给各种任务共享的,比如定时RPC调用,定时大数据处理,ForegroundHttp请求多任务并发。因此,在生产环境繁忙期间,可能会出现以下场景:线程池当前正在运行多个定时RPC调用和定时的大规模数据处理任务。这些任务特别耗时,导致很多线程忙碌。一些线程空闲。那么这个时候系统给C端用户提供的接口就有了高并发访问的场景。大量的Http请求过来,每个请求都要提交多个任务到线程池中并发运行,导致线程池中少数空闲线程快速运行。满了,然后大量的任务进入线程池的队列,开始排队等待。如下图6所示:这时候大量的Http请求必然会导致hang问题,因为很多Http请求任务在线程池中排队,无法运行,Http请求无法返回响应。用户感觉点击app/网页前端,但是点了点了没有反应,系统卡死了!如下图7所示:线程池高并发场景下的性能优化对于这个生产环境,我们首先要做的也是最大的改进就是将各种任务从一个线程池中分离出来,让它们互不影响。也就是说定时RPC任务放在一个线程池,定时DB海量数据处理任务放在另一个线程池,Http请求多任务并发处理放在独立的线程池,每个人使用自己的线程pool和resources互不影响。如下图8所示:如上图所示,我们有一个专门处理Http请求的线程池,压力立马减轻,因为Http请求的任务通常需要几十ms到100ms。整体速度非常快,线程池中没有定时RPC、定时DB访问等耗时任务。因此,Http请求的专用线程池可以轻松+愉快、快速地处理所有的Http请求任务。即使在高并发场景下,也可以通过线程池增加线程资源,合理抵御高并发压力。另一种是在线上系统的生产环境运行线程池任务。我们通常会在公司或者项目内部制定一个统一的线程池监控框架。所有的线程池任务都需要封装到线程池监控框架提供的一个类中,然后使用这个类实现任务排队和运行时间的二维监控数据统计。如下代码所示://线程任务包装类,采用装饰设计模式publicclassRunnableWrapperimplementsRunnable{//实际要执行的线程任务privateRunnabletask;//线程任务创建时间privatelongcreateTime;//线程池运行的线程任务的开始时间privatelongstartTime;//线程池运行的线程任务结束时间privatelongendTime;//创建这个任务的时候,会设置它的创建时间//但是之后有可能这个任务提交到线程池后,就会进入线程池队列publicRunnableWrapper(Runnabletask){this.task=任务;this.createTime=newDate().getTime();}//当任务在线程池中排队时,这个run方法不会被运行//但是当任务完成排队并有机会在线程池中运行时,这个方法就会被调用//此时,可以设置线程任务开始运行时间publicvoidrun(){this.startTime=newDate().getTime();//这里可以调用监控系统的API上报监控指标//使用线程任务的startTime-createTime,其实就是一个任务队列时间//monitor.report("threadName","queueWaitTime",开始时间-创建时间);//然后就可以调用被包装的实际任务的run方法task.run()了;//任务运行结束后,会设置任务运行结束时间this.endTIme=newDate().getTime();//这里可以调用监控系统的API,实现监控指标的上报//使用线程任务的endTime-startTime,其实就是任务运行时间//monitor.report("threadName","taskRunTime",endTime-startTime);}}通过上面的代码可以很清楚的看到,只要我们把所有的任务都提交到线程池中,所有的任务都统一封装在一个RunnableWrapper类的框架中,按照装饰方式进行封装,那么就可以得到线程任务的创建时间、开始时间、结束时间,就可以计算出这个任务的排队时间和运行时间。通过监控系统上报。这时,通过在监控系统中配置告警条件,我们就可以实现不同线程池中各个任务耗时指标的上报。阈值,将发出自动警报。如下图9所示:总结一下,今天的文章到此结束,给大家讲讲我们线程池在生产项目中的生产问题,如何优化高并发,以及生产环境中的监控方案。希望大家学以致用,以后在项目中使用线程池的时候,可以灵活运用我们文章中学到的知识点。
