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

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

1.概述

转载:Thread.Sleep 与 Thread.onSpinWait

2. 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

相关文章