当前位置: 首页 > 后端技术 > Java

这个面试题其实从11年前就开始讨论了,今年才正式表态

时间:2023-04-01 16:40:48 Java

大家好,我是伟伟。本期给大家呈现一道面试题,也就是下面的第二道题。这个面试题的图片都有些“古色古香”。所以为了大家的观感,我还是手打第二题。啧啧啧,这个行为,暖男作者还真锤了:spring在启动的时候会做classscanning,单例模式放到ioc里面。但是spring只处理每个类。如果为了加快速度,取消spring自带的类扫描功能,采用多线程的写代码并行处理的方式,这种方案可行吗?为什么?说实话,第一次看到这个面试题的时候,我也是一头雾水。我知道spring在启动的时候会把bean放到ioc容器里,但是真不知道是单线程的还是多线程的。所以我做的第一件事就是验证题中的那句话:butspringisjustoneclassforprocessing。如何验证?一定是在找源码,源码下没有什么秘密。如何找到它?这就需要你个人的经验去积累,还得去深挖Spring源码。这不是本文的重点,就不赘述了。但我可以教你一个我经常使用的技巧。首先你的项目中要有一个Bean,比如我这里的Person:然后调整项目日志级别为debug:logging.level.root=debug然后启动项目,在项目中找到Person的关键字。原理是这是一个bean。Spring在运行时会打印相关日志。从日志中反向查找代码要快得多。所以通过Debug日志,我们可以定位到这么一条关键的日志行:Identifiedcandidatecomponentclass:xxxx.Person.class]全局搜索关键字找到这个地方:这个地方就是第一个断点所在的地方。然后启动项目,从调用栈往前看,可以找到这个地方:这个类就是我要找的类:org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan从源码看确实没有并发里面的相关操作,看bean好像是在for循环中一个线程一个一个处理的。所以理论上,如果有两个没有任何关系的bean,比如我下面的两个bean,Person和Student,当它们交给Spring托管,放到ioc容器中时,就可以使用两个不同的Thread处理:所以问题来了:如果为了加快速度,我们取消spring自带的classscanning功能,使用写代码的多线程方式并行处理,这样行不行?我可以?我也不知道。但我知道去哪里寻找答案。不过在寻找答案之前,让我大胆猜一个答案:没有。为什么?因为我看的是Spring5.x版本的源码,在这个版本中还是单线程处理Bean。像Spring这种使用规模这么大的开源框架,如果能支持多线程加载,想必早就支持了。所以我会盲目猜测:没有。这个问题的答案一定隐藏在Spring的issues中。别问我为什么知道。这是老程序员的直觉。于是我直接来了:1.2kissues,怎么找我要的?一定是一波关键词搜索。根据你现在掌握的信息,关键词是什么?肯定是我们之前查到的方法和类,你掌握的信息也只有这个:org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan废话少说,先搜索类名,看看是什么Condition。从搜索结果来看,真的是一炮而红:带大家看看本期具体内容:https://github.com/spring-pro...有个同学叫kyangcmXF...呃,第一眼看到他的名字,看到F、K、C,第一个想到的就是《疯狂星期四》。那我就叫他“星期四”同学吧。《星期四》同学说:我的项目有几万个Bean需要Spring初始化。因此,每次项目启动时,都需要几分钟才能完成工作。然后他发现doScan的代码是单线程的,一个一个处理bean。于是他提出了一个问题:能不能用ConcurrentHashMap代替Setdataresults,然后并发加载。他的问题和我们文章开头提出的面试问题如出一辙。而且他还给出了实现代码:然后这个issue下只有一个回复,是这样的:首先我们来看看这个回复是谁:他是Spring的Contributors,他的回答可以说成为官方的。回答。他对他的“星期四”同学说:感谢老铁,但不可能。但目前无法异步后处理bean定义。至此,我们至少知道了异步加载的实现确实是有难度的,而不仅仅是单线程变多线程那么简单。然后,老头给“星期四”同学指路,说如果想了解更多,可以看看13410号的issue。虽然我们已经有了答案,但是既然老大已经展示了对了,我一定要带你去看看。我得从11年前说起。按照老大的指点,打开这个issue的时候惊呆了:https://github.com/spring-pro...标题翻译为《parallelduringstartupProcessingBeaninitialization》,和我们的采访密切相关问题。最让我震惊的是这个问题的创建时间:2011年10月12日。嘿伙计们,原来这个问题是在11年前提出和讨论的。但是根据我多年在github上冲浪的经验,遇到这种“年久失修”的问题,是无法从头到尾看完的。所以我只是把它拉到最后。没想到最后的回复还挺新鲜的。三个月前:回答的哥们也是Spring的正式员工,所以能看懂官方对这个问题的回答:这位哥们说了很多。很长一段,我简单翻译一下:他说这个问题不会在最新的6.0版本中解决,因为它目前的优先级不是特别高。在处理真正的启动案例时,我们常常会发现时间花在了几个特定的??相互依赖的bean上。在许多情况下,在那里引入并行化并不能节省太多,因为它不会加速关键路径。这通常与ORM设置和数据库迁移有关。您还可以使用应用程序启动跟踪为您自己的应用程序收集有关此的更多信息:您可以查看启动时间花费在何处以及如何花费,以及并行化是否会改善这一点。情况。对于SpringFramework6.0,我们专注于本机用例的AheadOfTime功能,以及启动时间改进。至此,再次印证了官方对bean并行处理的态度:但是这位哥们在回答中并没有说“这个功能不能做”,他说的是“经过研究,这样做之后的好处”功能实现不是很好”。而且他还透露了一个关键信息,对于Spring的启动速度,6.0中的方向是AOT。不是爆料,早在2020年,甚至更早,我记得Spring说未来的努力方向是AOT,提前编译(Ahead-of-TimeCompilation)。如果你对AOT很陌生,可以了解一下,不是本文的重点,提一下就好了。接下来,关于11年前的这个帖子,里面的内容还是很多的。我只能带你简单浏览一下帖子。想知道详情,还得自己去看看。首先,问这个问题的人其实已经提出了自己的解决方案:核心思想是在初始化bean的时候引入一个线程池,然后并发初始化bean。唯一需要特别考虑的是具有循环依赖性的bean。然后官方立马上线了:老哥,虽然从代码上看在Spring容器中引入并发bean初始化看起来直截了当,但是实现起来并没有看上去那么简单。重要的是我们需要看到更多的反馈和需求,当大家都在说“Spring容器初始化太慢了”的时候,我们会认真考虑这个改动。然后一个老哥跳出来说:我这边启动一个应用用了2小时30分钟。。。官方这时候也惊呆了:不过他们的核心点是:ParallelizeBeaninitializationintheSpringcontainerThebenefits对于少数使用Spring的应用程序非常重要,缺点是不可避免的错误、增加的复杂性和意外的副作用,这可能会影响所有使用Spring的应用程序,恐怕这不是一个有吸引力的强大前景。官方依然将这个问题定义为“won'tfix”,因为官方真的不太可能在没有充分理由的情况下对核心框架引入如此大的改动。这个观点也符合他的第一句话:morepragmaticapproach.moreeverybodyknows。approach应该也是一个耳熟能详的词:sopragmatic是什么意思?不知道这个词很正常,生僻字,但是你知道吗,我写技术文章的时候顺便教单词。Pragmatic,翻译过来就是“务实”的意思:那么“更务实的做法”是什么意思,快来和我一起大声朗读吧:更务实的做法。官方的意思是,更务实的做法是先找到启动慢的根源,而不是把问题归咎于Spring。关键是这是核心逻辑。如果没有充分的理由,能动就不要动。然后期间,用户和官方互相扯皮,直到5年后,也就是2016年6月30日:一个重要的官方决定:嗯,把这个问题的优先级提高到“主要”任务,留在5.0的积压工作。不过……好像官一波放鸽子了。一直到2018年,网友们都忍不住了,进展到什么地步了?没有回应。又是2019年了,进展如何,期待中:依旧没有回应。然后,时间来到了2020年,三年过去了,三年过去了,到现在已经9年了,老大,进度怎么样了?斗转星移,白马过隙,白云狗改天地。时光荏苒,转眼2021年,让我们共同祝贺本期十周年:终于到了今年7月15日,网友问:有什么好消息吗?官方回答:别问,我是鸽子,怎么了?怎么可能快?在寻找答案的过程中,我发现了这样一个项目:https://github.com/dsyer/spri...这个项目是对不同版本的SpringBoot启动时间的基准测试。测试的结论最终被政府采纳,所以还是很权威的。整个测试方法和测试过程,以及火焰图都贴在了链接里,我就不赘述了。我只是把最后的结论拿出来给大家看看:我会按照自己的理解来翻译。首先,如果您按照下面的方式进行操作,您将放弃一些功能,因此并非所有建议都适用于所有应用程序。从SpringBootweb启动器中排除以下类路径:HibernateValidator;Jackson(但SpringBoot执行器依赖于它)。如果需要JSON渲染,就用Gson;logback:用slf4j-jdk14代替spring-context-indexer,帮助不大,但有一点,算一点。如果可以,不要使用执行器。使用SpringBoot2.1和Spring5.1版本。当2.2和5.2可用时,升级到2.2和5.2版本,用spring.config.location(命令行参数或系统属性等)固定SpringBoot配置文件的位置。如果不需要JMX,请使用spring.jmx.enabled=false将其关闭(这是SpringBoot2.2中的默认设置)。设置Bean为lazy,即懒加载。SpringBoot2.2中有一个配置项spring.main.lazy-initialization=true。解压fatjar并使用显式类路径运行它。使用-noverify运行JVM。还要考虑-XX:TieredStopAtLevel=1。目的是关闭分层编译。至于每一点背后的原因,答案就隐藏在上面提到的问题中。感兴趣的可以自己去查查。我只是指出方法,所以我不会详细介绍。好了,就这些了,欢迎大家关注公众号《为什么要技术》,文章会全网发布。