Thread.Sleep 与 Thread.onSpinWait
Thread.Sleep
一般情况下,我们让线程等待一段时间都是使用Thread.sleep()
命令。比如下面这个demo示例:
@Test
public void test9() throws InterruptedException {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 开始执行");
try {
while (!b) {
TimeUnit.SECONDS.sleep(1);// 其内部也是调用的Thread.sleep实现的
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行完毕");
}).start();
TimeUnit.SECONDS.sleep(2);
b = true;
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " 执行完毕");
}
运行结果:
Thread-0 开始执行
Thread-0 执行完毕
main 执行完毕
如果我们想要停顿的时间足够短,取一个极端情况,等待时间为0millis: Thread.Sleep(0)
,那么每次都会停顿0毫秒,然后返回。
但是Thread.Sleep(0)
让线程挂起0millis的意义在哪里?意义在于这次调用Thread.Sleep(0)的当前线程暂时放弃cpu,让出CPU使用权,释放当前线程所剩余的时间片(如果有剩余的话),可以让操作系统切换其他线程来执行,就相当于一个让位动作。
然后由于CPU大多是抢占式的,这里让出了CPU,然后停顿0millis,继续去抢占CPU,这个时候不一定会抢得到哦,CPU可能会被其他线程抢走。
在线程没退出之前,线程有三个状态,就绪态,运行态,等待态。sleep(n)之所以在n秒内不会参与CPU竞争,是因为,当线程调用sleep(n)的时候,线程是由运行态转入等待态,线程被放入等待队列中,等待定时器n秒后的中断事件,当到达n秒计时后,线程才重新由等待态转入就绪态,被放入就绪队列中,等待队列中的线程是不参与cpu竞争的,只有就绪队列中的线程才会参与cpu竞争,所谓的cpu调度,就是根据一定的算法(优先级,FIFO等。。。),从就绪队列中选择一个线程来分配cpu时间。
而sleep(0)之所以马上回去参与cpu竞争,是因为调用sleep(0)后,因为0的原因,线程直接回到就绪队列,而非进入等待队列,只要进入就绪队列,那么它就参与cpu竞争。
综上所述,如果想要在循环中让线程等待一段时间,但是又想等待的时间足够短,那么就可以使用Thread.Sleep(0)
。这样比直接空循环会节省一些CPU资源。但是每次线程都要挂起转移到等待队列,这也会消耗不少时间,因此即使设置的是等待0,他还是会有一点点的等待时间。
Thread.onSpinWait
onSpinWait方法默认是空实现。它被@HotSpotIntrinsicCandidate
修饰。JDK的源码中,被@HotSpotIntrinsicCandidate标注的方法,在HotSpot中都有一套高效的实现,该高效实现基于CPU指令,运行时,HotSpot维护的高效实现会替代JDK的源码实现,从而获得更高的效率。
具体HotSpot做了哪些优化,后续再研究吧。或者哪位大神知道可以分享一下。
@HotSpotIntrinsicCandidate
public static void onSpinWait() {}
int a = 1000000,b=1000000,c=1000000;
@Test
public void test10() throws InterruptedException {
new Thread(() -> {
long begin = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " 开始执行");
while (a-->0) {
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 执行完毕 " + (System.currentTimeMillis() - begin));
}, "a").start();
new Thread(() -> {
long begin = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " 开始执行");
while (b-->0) {
Thread.onSpinWait();
}
System.out.println(Thread.currentThread().getName() + " 执行完毕 " + (System.currentTimeMillis() - begin));
}, "b").start();
new Thread(() -> {
long begin = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " 开始执行");
while (c-->0) {
}
System.out.println(Thread.currentThread().getName() + " 执行完毕 " + (System.currentTimeMillis() - begin));
}, "c").start();
Thread.sleep(1000000000);
}
输出:
a 开始执行
b 开始执行
c 开始执行
c 执行完毕 21
b 执行完毕 60
a 执行完毕 500
上述程序,循环等待100W次。使用Thread.onSpinWait();
比Thread.sleep(0);
性能要好。
但是c线程空循环耗时最少。不过区别在于空循环不会使用到虚拟机的优化。
官方文档提到:
The code above would remain correct even if the {@code onSpinWait} method was not called at all. However on some architectures the Java Virtual Machine may issue the processor instructions to address such code patterns in a more beneficial way.
简单翻译一下就是:即使不调用Thread.onSpinWait();
程序也可以正常运行,但是在一些架构中,HotSpot虚拟机会对其进行优化。
总结
- 使用
Thread.onSpinWait();
比Thread.sleep(0);
性能要好。 - Thread.onSpinWait(); 到底比空循环好在哪里,还没有搞清楚。
- JDk源码中应用也比较少:Phaser, StampedLock,SynchronousQueue