欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Thread.Sleep 与 Thread.onSpinWait

程序员文章站 2022-03-08 10:07:08
...

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
相关标签: 源码