Java中不同忙循环实现下Thread.sleep(1)开销差异成因问询
结论
这个开销差异既不是编译器循环展开这类优化导致的,也和for/while的循环语法差异没关系,核心是两个测试的Thread.sleep(1)调用逻辑本质不同,再叠加操作系统线程调度的固有偏差累积,最终出现了耗时差。
具体分析
1. 先排除编译器优化的干扰
几个硬特征可以直接把优化的可能性排除:
Thread.sleep()是JNI本地方法,JIT编译器不会对包含本地方法调用的循环做循环展开、循环消除这类激进优化。- 第二个测试的循环判断条件是
volatile boolean flag,Java内存模型要求volatile变量的读必须每次取最新值,禁止编译器把flag判断逻辑挪到循环外、或者提前判定循环终止,根本不存在优化掉循环执行次数的可能。 - 测试覆盖Java 11、Java 18两个大版本,Windows、Linux两个系统平台,都呈现一致趋势,也能排除特定JVM版本的编译优化bug或者特殊策略的影响。
2. 核心差异:固定次数调用 vs 固定时间窗口等待
两个测试表面看都是循环调用Thread.sleep(1),但循环终止逻辑完全不是一回事:
- 第一个
ThreadSleep1Benchmark是固定执行delay次sleep(1)调用:不管每次sleep实际睡了多久,必须跑完指定次数才退出。
所有操作系统的sleep(1)都不可能精准卡1ms:线程调用sleep后会从运行态挂起,等休眠时间到了,还要等操作系统调度器给它分配CPU时间片才能真正醒过来继续执行,每次调用都会产生一次固定的挂起+唤醒调度开销。这个开销会跟着调用次数线性累积:delay=5就累积5次开销,delay=50就累积50次,总耗时自然会比目标delay值高一大截。 - 第二个
ThreadSleep2Benchmark是等够delay毫秒就退出:后台线程只调用一次sleep(delay),到点就把flag改成false;前台线程每次醒了查下flag,没到点就再睡1ms,循环跑多少次根本不固定。
这个逻辑下会自然抵消掉一部分调度开销:如果前台线程某次sleep(1)因为调度延迟睡超了(比如本来打算睡1ms,结果内核调度卡了1.3ms才把它唤醒),就会少跑一次sleep调用,直接省掉一次挂起唤醒的固定成本。delay越大,这种“睡超了少调一次sleep”的情况出现得越多,和固定跑N次的第一个测试的耗时差就越明显。实测数值完全对得上这个规律:delay=5的时候总共没几次循环,省不了多少开销,两个测试成绩几乎没差;delay=50的时候累积省了十几次调用的成本,总耗时就低了十几毫秒。
3. Linux平台的测试结果刚好能佐证这个逻辑
Linux默认开启高分辨率定时器,进程调度延迟比Windows低,每次sleep(1)的挂起唤醒开销更小,所以两个测试的绝对耗时都比Windows环境低,两者的差值也更小——如果差异是编译器优化导致的,不可能换个操作系统就出现同比例的耗时变化。
内容的提问来源于stack exchange,提问作者Sergey Tsypanov
相关产品推荐
相关产品推荐

