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

没有二十年的功力,写不出Thread.sleep(0)这行“看似无用”的代码!

时间:2023-04-01 21:30:47 Java

大家好,我是喜欢呆在家7天的歪歪。这篇文章开头有个奇怪的评论,就是下图:具体的代码逻辑我们可以忽略,只看for循环。在循环中,有一个特殊的变量j,用来记录当前的循环次数。在第一个循环和此后每1000个循环之后,输入一个if逻辑。在这个if逻辑之上,标注了一条注释:preventgc.prevent。如果你不会这个词,记住它,你肯定要考试:这个注释翻译过来就是:防止GC线程进行垃圾回收。具体实现逻辑如下:核心逻辑其实就是这么一行代码:Thread.sleep(0);这样preventgc可以实现吗?使困惑?不懂事就对了,懂事就值得玩。这段代码片段其实是来自RocketMQ的源码:org.apache.rocketmq.store.logfile.DefaultMappedFile#warmMappedFile需要提前说明的是,我没有找到写这段代码的人,问一下他的用意是什么,所以我只需要根据自己的理解来推断他的意图。如果猜错了,请指教。虽然这是RocketMQ的源码,但根据我的理解,这个小技巧与RocketMQ框架无关,可以脱离框架存在。我给的修改是这样的:把int改成long,然后for循环里面的if逻辑就可以直接去掉了。这不是让你更迷茫吗?别慌,接下来,我给你抽茧。另外,在“抽丝剥茧”之前,我先做一个总结:提出这个修改方案的理论依据是Java的安全点相关的知识,即safepoint。官方最终没有采纳这个修正案。正式采纳与否并不重要,重要的是我给你“剥茧”。探索当知道这段代码属于RocketMQ时,我第一时间想到的就是从代码提交记录中寻找答案。看提交者在提交代码时是否表明了自己的意图。于是我把代码拉下来,看到commit记录是这样的:我就知道这里不会有答案。因为这个类在第一次提交的时候就已经包含了这个逻辑,而且这次提交对应的代码也很多,并没有具体说明对应的功能。没有从提交日志中获得有用的信息。于是把目光转向了github的issue,搜索关键词preventgc。除了第一个链接,没有找到有用的信息:第一个链接对应的issues是这个:https://github.com/apache/roc...这个issue其实就是我们针对这个issue所讨论的whatwasproposed过程中就是之前出现的修改方案:也就是说,我想通过源码或者github找到这个问题的权威答案,但是没有找到。于是我又去了这个神奇的网站,发现了这个2018年提出的问题:https://stackoverflow.com/que...问题和我们的一模一样,不过这里是这个问题的答案:thisTheanswer不好,因为我觉得这个答案不对,不过没关系,我可以直接以这个答案为出发点,对齐差点,赋能。看这个回答的第一句话:Itdoesnot(它没有)。问题来了:“它”是谁?“没有什么?”指的是之前出现的代码,“No”表示没有阻止GC线程进行垃圾回收,这个回答说:调用Thread.sleep(0)的目的是为了给GC线程有机会被操作系统选择执行垃圾清理。它的副作用是可能会更频繁地运行GC,毕竟你每1000次迭代就有机会运行GC,但好处是它可以防止longgarbagecollections.换句话说,这段代码是想“触发”GC,而不是“避免”GC,或者“避免”耗时较长的GC。从这个角度来看,程序中的注释实际上是在撒谎或不完整。它不是防止gc,而是对gc采用“分手运行,削峰填谷”的思想,从而防止长时间gc。但是你想想,我们自己编程的时候,一般情况下,“这个地方应该触发GC”的想法永远不会冒出来吧?因为我们知道,对于Java程序员来说,虚拟机是有自己的GC机制的。我们不需要像编写C或C++那样自己管理内存。我们只需要关注业务代码,不需要特别关注GC机制。那么本文最关键的问题来了:为什么要在此处的代码中特别关注GC,并想尝试“触发”GC?先说答案:safepoint,安全点。关于安全点的描述,可以看《深入理解JVM虚拟机(第三版)》的3.4.2节:注意书中的描述:设置了安全点后,判断用户程序在任何位置都没有执行代码指令流。执行必须到达安全点才能暂停,而不是能够暂停以启动垃圾收集。也就是说:没有到达安全点,就不能进行STW,所以可以进行GC。如果在你的认知中,GC线程是可以随时运行的。那么你需要刷新你的理解。接下来,让我们将注意力转向本书的第5.2.8节:SafepointsCausedLongPauses。里面有这么一段话:划线的部分我单独拿出来,大家可以仔细阅读:为了避免安全点过多造成的负担过重,HotSpot虚拟机还有一个循环的优化措施,认为循环次数比较少。如果小于,则执行时间不宜过长,这样使用int类型或范围更小的数据类型作为索引值的循环,默认不会放在安全点。这种循环称为可数循环(CountedLoop)。相应地,使用long或更大数据类型作为索引值的循环称为未计数循环(UncountedLoop),将被放在安全的地方。.意味着在可数循环(CountedLoop)的情况下,HotSpot虚拟机做了一个优化,即直到循环结束,线程才会进入安全点。反之,如果循环没有结束,线程就不会进入安全点,GC线程就必须等待当前线程循环结束进入安全点,才能开始工作。什么是可数循环(CountedLoop)?书中的案例来自这个链接:https://juejin.cn/post/684490...HBase实战:记一次因为Safepoint导致长STW的踩坑之旅如果有时间,我建议你把这个case完整看一下,我只截取解题部分:截图中的while(i{for(inti=0;i<1000000000;i++){num.getAndAdd(1);}System.out.println(Thread.currentThread().getName()+"执行结束!");};线程t1=新线程(可运行);线程t2=新线程(可运行);t1.开始();t2.开始();线程.睡眠(1000);System.out.println("num="+num);}}您可以将此代码直接粘贴到您的想法中并运行它。根据代码,主线程休眠1000ms后会输出结果,但实际情况是主线程一直在等待t1和t2完成,才继续执行。这个循环属于前述的可数循环(CountedLoop)。这个程序怎么了?1.开始两个不间断的长循环(内部没有安全点检查)。2.主线程休眠1秒。3.1000毫秒后,JVM尝试在安全点停止,以便Java线程进行定期清理,但直到可数循环完成后才能这样做。4、主线程的Thread.sleep方法从native返回,发现正在进行safepoint操作,于是自己挂起,直到操作结束。因此,当我们将int改为long时,程序表现正常:受RocketMQ源码的启发,我们也可以直接拿它的代码:这样,即使for循环的对象是int类型,也可以执行不出所料。因为我们相当于在循环体中插入了Safepoint。另外,我以一种不严谨的方式测试了两种方案的耗时:我在自己的机器上跑了几次,时间上相差不大。但是要说force,就得在右边写preventgc。没有二十年的功力,写不出这行“看似无用”的代码!还要提一下也是之前RocketMQ的源码引起的一个思考:这个方法是干什么的?预热文件,根据4K的大小,在byteBuffer中放入0,对文件进行预热。byteBuffer.put(i,(byte)0);为什么我对这个4k热身更敏感?去年的天池比赛有这样一个赛道:https://tianchi.aliyun.com/co...其中有两位选手提到了“文件预热”的想法。我把链接放在下面,有兴趣的可以仔细看一下:https://tianchi.aliyun.com/fo...https://tianchi.aliyun.com/fo...最后谢谢大家阅读我的文章。欢迎关注公众号【为什么科技】,文章将全网发布。