【java】Thread.Sleep(0) 与 Thread.onSpinWait

x33g5p2x  于2022-02-15 转载在 Java  
字(3.2k)|赞(0)|评价(0)|浏览(416)

1.概述

转载:Thread.Sleep 与 Thread.onSpinWait

2. Thread.Sleep

一般情况下,我们让线程等待一段时间都是使用Thread.sleep()命令。比如下面这个demo示例:

  1. @Test
  2. public void test9() throws InterruptedException {
  3. new Thread(() -> {
  4. System.out.println(Thread.currentThread().getName() + " 开始执行");
  5. try {
  6. while (!b) {
  7. TimeUnit.SECONDS.sleep(1);// 其内部也是调用的Thread.sleep实现的
  8. }
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. System.out.println(Thread.currentThread().getName() + " 执行完毕");
  13. }).start();
  14. TimeUnit.SECONDS.sleep(2);
  15. b = true;
  16. TimeUnit.SECONDS.sleep(2);
  17. System.out.println(Thread.currentThread().getName() + " 执行完毕");
  18. }

运行结果:

  1. Thread-0 开始执行
  2. Thread-0 执行完毕
  3. 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做了哪些优化,后续再研究吧。或者哪位大神知道可以分享一下。

  1. @HotSpotIntrinsicCandidate
  2. public static void onSpinWait() {}
  1. int a = 1000000,b=1000000,c=1000000;
  2. @Test
  3. public void test10() throws InterruptedException {
  4. new Thread(() -> {
  5. long begin = System.currentTimeMillis();
  6. System.out.println(Thread.currentThread().getName() + " 开始执行");
  7. while (a-- > 0) {
  8. try {
  9. Thread.sleep(0);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. System.out.println(Thread.currentThread().getName() + " 执行完毕 " + (System.currentTimeMillis() - begin));
  15. }, "a").start();
  16. new Thread(() -> {
  17. long begin = System.currentTimeMillis();
  18. System.out.println(Thread.currentThread().getName() + " 开始执行");
  19. while (b-- > 0) {
  20. Thread.onSpinWait();
  21. }
  22. System.out.println(Thread.currentThread().getName() + " 执行完毕 " + (System.currentTimeMillis() - begin));
  23. }, "b").start();
  24. new Thread(() -> {
  25. long begin = System.currentTimeMillis();
  26. System.out.println(Thread.currentThread().getName() + " 开始执行");
  27. while (c-- > 0) {
  28. }
  29. System.out.println(Thread.currentThread().getName() + " 执行完毕 " + (System.currentTimeMillis() - begin));
  30. }, "c").start();
  31. Thread.sleep(1000000000);
  32. }

输出:

  1. a 开始执行
  2. b 开始执行
  3. c 开始执行
  4. c 执行完毕 21
  5. b 执行完毕 60
  6. 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

相关文章

最新文章

更多