关于后端:没有二十年功力写不出Threadsleep0这一行看似无用的代码

43次阅读

共计 4807 个字符,预计需要花费 13 分钟才能阅读完成。

你好呀,我是喜提七天居家隔离的歪歪。

这篇文章要从一个奇怪的正文说起,就是上面这张图:

咱们能够不必管具体的代码逻辑,只是单单看这个 for 循环。

在循环外面,专门有个变量 j,来记录以后循环次数。

第一次循环以及往后每 1000 次循环之后,进入一个 if 逻辑。

在这个 if 逻辑之上,标注了一个正文:prevent gc.

prevent,这个单词如果不意识的同学记一下,考试必定要考的:

这个正文翻译一下就是:避免 GC 线程进行垃圾回收。

具体的实现逻辑是这样的:

外围逻辑其实就是这样一行代码:

Thread.sleep(0);

这样就能实现 prevent gc 了?

懵逼吗?

懵逼就对了,懵逼就阐明值得把玩把玩。

这个代码片段,其实是出自 RocketMQ 的源码:

org.apache.rocketmq.store.logfile.DefaultMappedFile#warmMappedFile

当时须要阐明的是,我并没有找到写这个代码的人问他的用意是什么,所以我只有基于本人的了解去揣测他的用意。如果揣测的不对,还请多多指教。

尽管这是 RocketMQ 的源码,然而基于我的了解,这个小技巧和 RocketMQ 框架没有任何关系,齐全能够脱离于框架存在。

我给出的修改意见是这样的:

把 int 批改为 long,而后就能够间接把 for 循环外面的 if 逻辑删除掉了。

这样一看是不是更加懵逼了?

不要慌,接下来,我给你抽丝剥个茧。

另外,在“剥茧”之前,我先说一下论断:

  • 提出这个批改计划的实践立足点是 Java 的平安点相干的常识,也就是 safepoint。
  • 官网最初没有驳回这个批改计划。
  • 官网采没驳回不重要,重要的是我高下得给你“剥个茧”。

摸索

当我晓得这个代码片段是属于 RocketMQ 的时候,我想到的第一个点就是从代码提交记录中寻找答案。

看提交者是否在提交代码的时候阐明了本人的用意。

于是我把代码拉了下来,一看提交记录是这样的:

我就晓得这里不会有答案了。

因为这个类第一次提交的时候就曾经蕴含了这个逻辑,而且对应这次提交的代码也十分多,并没有特地阐明对应的性能。

从提交记录上没有取得什么有用的信息。

于是我把眼光转向了 github 的 issue,拿着关键词 prevent gc 搜寻了一番。

除了第一个链接之外,没有找到什么有用的信息:

而第一个链接对应的 issues 是这个:

https://github.com/apache/roc…

这个 issues 其实就是咱们在探讨这个问题的过程中提出来的,也就是后面呈现的批改计划:

也就是说,我想通过源码或者 github 找到这个问题权威的答复,是找不到了。

于是我又去了这个神奇的网站,在外面找到了这个 2018 年提出的问题:

https://stackoverflow.com/que…

问题和咱们的问题截然不同,然而这个问题上面就这一个答复:

这个答复并不好,因为我感觉没答到点上,然而没关系,我刚好能够把这个答复作为抓手,把差的这一点拉通对齐一下,给它赋能。

先看这个答复的第一句话:It does not(它没有)。

问题就来了:“它”是谁?“没有”什么?

“它”,指的就是咱们后面呈现的代码。

“没有”,是说没有避免 GC 线程进行垃圾回收。

这个的答复说:通过调用 Thread.sleep(0) 的目标是为了让 GC 线程有机会被操作系统选中,从而进行垃圾清理的工作。它的副作用是,可能会更频繁地运行 GC,毕竟你每 1000 次迭代就有一次运行 GC 的机会,然而益处是能够避免长时间的垃圾收集。

换句话说,这个代码是想要“触发”GC,而不是“防止”GC,或者说是“防止”工夫很长的 GC。从这个角度来说,程序外面的正文其实是在扯谎或者没写残缺。

不是 prevent gc,而是对 gc 采取了“打散运行,削峰填谷”的思维,从而 prevent long time gc。

然而你想想,咱们本人编程的时候,失常状况下素来也没冒出过“这个中央应该触发一下 GC”这样想法吧?

因为咱们晓得,Java 程序员来说,虚拟机有本人的 GC 机制,咱们不须要像写 C 或者 C++ 那样得本人治理内存,只有关注于业务代码即可,并没有特地留神 GC 机制。

那么本文中最要害的一个问题就来了:为什么这里要在代码外面特地留神 GC,想要尝试“触发”GC 呢?

先说答案:safepoint,平安点。

对于平安点的形容,咱们能够看看《深刻了解 JVM 虚拟机(第三版)》的 3.4.2 大节:

留神书外面的形容:

有了平安点的设定,也就决定了用户程序执行时并非在代码指令流的任意地位都可能停顿下来开始垃圾收集,而是强制要求必须执行达到平安点后才可能暂停。

换言之:没有到平安点,是不能 STW,从而进行 GC 的。

如果在你的认知外面 GC 线程是随时都能够运行的。那么就须要刷新一下认知了。

接着,让咱们把眼光放到书的 5.2.8 大节:由平安点导致长时间进展。

外面有这样一段话:

我把划线的局部独自拿进去,你认真读一遍:

是 HotSpot 虚拟机为了防止平安点过多带来过重的累赘,对循环还有一项优化措施,认为循环次数较少的话,执行工夫应该也不会太长,所以应用 int 类型或范畴更小的数据类型作为索引值的循环默认是不会被搁置平安点的。这种循环被称为可数循环(Counted Loop),绝对应地,应用 long 或者范畴更大的数据类型作为索引值的循环就被称为不可数循环(Uncounted Loop),将会被搁置平安点。

意思就是在可数循环(Counted Loop)的状况下,HotSpot 虚拟机搞了一个优化,就是等循环完结之后,线程才会进入平安点。

反过来说就是:循环如果没有完结,线程不会进入平安点,GC 线程就得等着以后的线程循环完结,进入平安点,能力开始工作。

什么是可数循环(Counted Loop)?

书外面的这个案例来自于这个链接:

https://juejin.cn/post/684490…
HBase 实战:记一次 Safepoint 导致长时间 STW 的踩坑之旅

如果你有工夫,我倡议你把这个案例残缺的看一下,我只截取问题解决的局部:

截图中的 while(i < end) 就是一个可数循环,因为执行这个循环的线程须要在循环完结后才进入 Safepoint,所以先进入 Safepoint 的线程须要期待它。从而影响到 GC 线程的运行。

所以,批改计划就是把 int 批改为 long。

原理就是让其变为不可数循环(Uncounted Loop),从而不必等循环完结,在循环期间就能进入 Safepoint。

接着咱们再把眼光拉回到这里:

这个循环也是一个可数循环。

Thread.sleep(0) 这个代码看起来莫名其妙,然而我是不是能够大胆的猜想一下:成心写这个代码的人,是不是为了在这里搁置一个 Safepoint 呢,以达到防止 GC 线程长时间期待,从而加长 stop the world 的工夫的目标?

所以,我接下来只须要找到 sleep 会进入 Safepoint 的证据,就能证实我的猜测。

你猜怎么着?

原本是想去看一下源码,后果啪的一下,在源码的正文外面,间接找到了:

https://hg.openjdk.java.net/j…

正文外面说,在程序进入 Safepoint 的时候,Java 线程可能正处于框起来的五种不同的状态,针对不同的状态有不同的解决计划。

原本我想一个个的翻译的,然而信息量太大,我消化起来有点费劲儿,所以就不乱说了。

次要聚焦于和本文相干的第二点:Running in native code。

When returning from the native code, a Java thread must check the safepoint _state to see if we must block.

第一句话,就是答案,意思就是一个线程在运行 native 办法后,返回到 Java 线程后,必须进行一次 safepoint 的检测。

同时我在知乎看到了 R 大的这个答复,外面有这样一句,也印证了这个点:

https://www.zhihu.com/questio…

那么接下来,就是见证奇观的时刻了:

依据 R 大的说法:正在执行 native 函数的线程看作“曾经进入了 safepoint”,或者把这种状况叫做“在 safe-region 里”。

sleep 办法就是一个 native 办法,你说巧不巧?

所以,到这里咱们能够确定的是:调用 sleep 办法的线程会进入 Safepoint。

另外,我还找到了一个 2013 年的 R 大对于相似问题探讨的帖子:

https://hllvm-group.iteye.com…

这里就间接点名道姓的指出了:Thread.sleep(0).

这让我想起以前有个面试题问:Thread.sleep(0) 有什么用。

过后我就想:这题真难(S)啊(B)。当初发现原来是我道行不够,小丑竟是我本人。

还真的是有用。

实际

后面其实说的都是实践。

这一部分咱们来拿代码实际跑上一把,就拿我之前分享过的《真是绝了!这段被 JVM 动了手脚的代码!》文章外面的案例。

public class MainTest {public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {Runnable runnable=()->{for (int i = 0; i < 1000000000; i++) {num.getAndAdd(1);
            }
            System.out.println(Thread.currentThread().getName()+"执行完结!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("num =" + num);
    }
}

这个代码,你间接粘到你的 idea 外面去就能跑。

依照代码来看,主线程休眠 1000ms 后就会输入后果,然而理论状况却是主线程始终在期待 t1,t2 执行完结才继续执行。

这个循环就属于后面说的可数循环(Counted Loop)。

这个程序产生了什么事件呢?

  • 1. 启动了两个长的、不间断的循环(外部没有平安点查看)。
  • 2. 主线程进入睡眠状态 1 秒钟。
  • 3. 在 1000 ms 之后,JVM 尝试在 Safepoint 进行,以便 Java 线程进行定期清理,然而直到可数循环实现后能力执行此操作。
  • 4. 主线程的 Thread.sleep 办法从 native 返回,发现平安点操作正在进行中,于是把本人挂起,直到操作完结。

所以,当咱们把 int 批改为 long 后,程序就体现失常了:

受到 RocketMQ 源码的启发,咱们还能够间接把它的代码拿过去:

这样,即便 for 循环的对象是 int 类型,也能够依照预期执行。因为咱们相当于在循环体中插入了 Safepoint。

另外,我通过 不谨严的形式 测试了一下两个计划的耗时:

在我的机器上运行了几次,工夫上都差距不大。

然而要论逼格的话,还得是左边的 prevent gc 的写法。没有二十年功力,写不出这一行“看似无用”的代码!

额定提一句

再说一个也是由后面的 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…

最初,感激你浏览我的文章。欢送关注公众号【why 技术】,文章全网首发哦。

正文完
 0