Java中哪个更快,wait-notify还是busy-wait?

16

我知道使用忙等待不是一个好的编程实践,最好尽可能使用同步对象(wait-notify)。但如果可以牺牲CPU周期,我想知道忙等待和wait-notify哪个更快?

我认为,wait-notify将涉及到同步对象上的内部锁,并且信号可能来自内核以唤醒线程,使这种方法比忙等待慢得多,而在忙等待中,可以持续检查一个条件,直到满足条件。一旦满足条件(例如布尔值== true),线程就可以退出忙等待。根据我的理解,我认为忙等待应该更快。

如果其他人能分享他们的想法并纠正我的观点,我将不胜感激。


2
我可能错了,但如果强制CPU循环,那么它的速度不会比CPU空闲时更慢吗? - MadProgrammer
2
我无法相信在现代系统中,你不是直接写入硬件,而是运行在操作系统上并通过驱动程序和不知道多少个等待IO的操作系统代码,忙等待会比阻塞显着更快。但是,当你有疑问时,可以尝试两种方式并测量结果! - markspace
2
写一个实验会比写一个问题需要更长的时间吗?我怀疑你会发现,一个快速的实验会让你对正确的方法毫无疑问。 - T.J. Crowder
3
使用 wait()/notify() 的优势在于一旦调用 notify(),等待中的线程之一将被通知并开始执行。也就是说,调用 notify() 的线程不会继续执行。如果忙等待,即使第二个线程设置了第一个线程正在等待的布尔标志,第二个线程仍将执行直到其时间片段完成,然后第一个线程才开始执行。如有错误请其他人指正。 - TheLostMind
2
@TheLostMind notify() 不会导致调用线程挂起,也不会立即唤醒等待的线程。相反,被通知的线程仍然需要等待通知线程先离开同步块。关于时间片问题:在多核系统中,线程可能真正并行运行!因此你的说法并不完全正确。 - isnot2bad
显示剩余7条评论
4个回答

16

实验表明,如果你使用忙等待,你会比等待并通知(在我的硬件上,至少是这样)更早地看到标志。 (详细信息如下。)差异非常非常非常非常小,因此这仅适用于非常罕见的应用程序。例如股票交易应用程序,其中公司正在寻求他们可以获得的任何优势(争取将其服务器定位在距离交易所尽可能近的位置,以获得网络反馈中微秒级的改进等),可能认为这种差异是值得的。我还可以想象一些科学应用。

在绝大多数应用程序中,差异实际上没有任何区别。

但是,对于CPU来说会出现一个核心的硬长期占用:

hardpeg

这对于影响盒子上的其他进程和数据中心中的功耗都不好。

因此:只有在真正重要的情况下,才极度勉强地使用。


数据(非常小的样本,但代码如下):

繁忙等待:10631 12350 15278
等待并通知:87299 120964 107204
Delta:76668 108614 91926

时间以纳秒为单位。十亿分之一秒。上面的平均增量为92403ns(0.092402667毫秒,0.000092403秒)。

BusyWait.java

public class BusyWait {

    private static class Shared {
        public long setAt;
        public long seenAt;
        public volatile boolean flag = false;
    }

    public static void main(String[] args) {
        final Shared shared = new Shared();
        Thread notifier = new Thread(new Runnable() {
            public void run() {
                System.out.println("Running");
                try {
                    Thread.sleep(500);
                    System.out.println("Setting flag");
                    shared.setAt = System.nanoTime();
                    shared.flag = true;
                }
                catch (Exception e) {
                }
            }
        });
        notifier.start();
        while (!shared.flag) {
        }
        shared.seenAt = System.nanoTime();
        System.out.println("Delay between set and seen: " + (shared.seenAt - shared.setAt));
    }
}

WaitAndNotify.java:

等待和通知(WaitAndNotify).java:
public class WaitAndNotify {

    private static class Shared {
        public long setAt;
        public long seenAt;
        public boolean flag = false;
    }

    public static void main(String[] args) {
        (new WaitAndNotify()).test();
    }
    private void test() {
        final Shared shared = new Shared();
        final WaitAndNotify instance = this;
        Thread notifier = new Thread(new Runnable() {
            public void run() {
                System.out.println("Running");
                try {
                    Thread.sleep(500);
                    System.out.println("Setting flag");
                    shared.setAt = System.nanoTime();
                    shared.flag = true;
                    synchronized (instance) {
                        instance.notify();
                    }
                }
                catch (Exception e) {
                }
            }
        });
        notifier.start();
        while (!shared.flag) {
            try {
                synchronized (this) {
                    wait();
                }
            }
            catch (InterruptedException ie) {
            }
        }
        shared.seenAt = System.nanoTime();
        System.out.println("Delay between set and seen: " + (shared.seenAt - shared.setAt));
    }
}

2
通常情况下,while循环应该在同步块内部,而不是相反的情况,对吗? - isnot2bad
1
@isnot2bad:这取决于你在做什么。通常的规则是尽可能少地同步。在这种情况下,这意味着只在waitnotify调用周围进行同步。当然,在理论上,我们实际上从来没有循环(因为那需要除了我们的升旗代码之外的其他东西执行notify),我们只进入while的主体一次,然后永远不会重复,所以对于这个测试代码,无论哪种方式都是一样的。 - T.J. Crowder
@T.J.Crowder 你是对的。另一件事是,你正在同步块之外修改共享标志。(我知道在你的情况下它可以工作,因为以下同步块确保了一个“发生在之前”的关系,但仍然...)。顺便说一下,我稍微修改了你的代码,并在循环中调用它进行JVM预热,这导致繁忙等待示例的大幅加速。 - isnot2bad
如果期望的等待时间超过线程剩余的时间片,则旋转的任何优势都会完全消失。如果线程被交换出去,则首选wait/notify。 - Jim Mischel
@JimMischel: 是的,我记得原帖中提到了假设有足够的核心,但现在看不到了。我在我的四核机器上进行了20秒的测试,上面运行着三个虚拟机和其他各种东西(Linux)。数字与我以上半秒测试一致。但是我的时间片知识很少,其他核心也没有负载,所以我猜测线程被允许保持核心。 - T.J. Crowder

5

在忙等待的情况下,人们准备牺牲CPU周期以获取更快的速度。

一个关于忙等待实时低延迟应用的例子是,有一个名为LMAX Disruptor 的框架,它是为伦敦证券交易所打造的,其中一种锁定策略是忙等待,并且这就是他们使用的方式。

为了实现超高速,最好浪费CPU周期而不是浪费在等待锁通知上的时间。

您对其他内容的理解也是正确的,如果您在Google上搜索Disruptor并阅读他们的论文,您会获得更多的澄清。关于高性能和低延迟,还有太多要说的。

一个不错的博客可以参考机械同理心


1

这要看情况:

情况A

如果'busy-wait'正在等待的是硬件操作(例如,将硬盘扇区读入内存):

1)硬件会执行该操作。

2)驱动程序将启动中断。

3)操作将暂停实际进程(即您的busy-wait进程),保存中断处理中覆盖的任何CPU寄存器的实际值。

4)中断将被处理,修改任何指示数据可用的标志(例如,在读取磁盘时)。

5)覆盖的寄存器将被恢复。

6)您的进程将继续进行。在下一次迭代中,它将调用循环的条件。例如,如果busy wait是:

while( !fileReady() ){
    ...
}

fileReady()方法将会是一个内部方法,它将检查一个具体的标志(在第4步中被修改的标志)是否被设置。 7) 所以在下一次迭代中,循环将进入并执行操作。
请注意,如果有其他进程正在运行(操作系统进程、其他程序),它们将把您的进程放到进程队列的尾部。此外,操作系统可以决定,如果您的进程已经使用了所有它能够使用的CPU周期(它花费了它的时间片),那么它将比其他进程的优先级低,而这些进程在需要等待某个条件时会进入睡眠状态(而不是使用繁忙等待方法)。
结论。如果没有其他外部进程在同一核心/CPU上运行(非常不可能),那么速度会更快。

方案B

另一方面,如果繁忙方法正在等待另一个进程结束(或将任何变量设置为特定值),则繁忙等待将变慢。

1)繁忙方法将在CPU上运行。由于其他进程未运行,条件无法更改,因此繁忙方法将一直运行,直到CPU决定将CPU时间分配给另一个进程。

2)其他进程将运行。如果该进程花费的时间没有达到繁忙等待进程所需的条件,则返回第1步,否则继续执行第3步。

3)其他(非繁忙等待)进程仍将运行一段时间,直到CPU决定更换新进程。

4)繁忙方法将再次运行,但此时条件已满足,因此操作现在已完成。

结论:速度较慢,并且我们还会减缓整个过程。


方案 C

如果我们有与方案 B 相同的情况,但有多个核心(每个核心一个进程),该怎么办?

首先,请记住,即使您拥有具有多个核心的 CPU,您的程序可能也不允许使用超过一个核心。而且可能有一些操作系统或其他程序正在使用它们。

其次,这并不值得。请记住,进程必须进行通信,以便繁忙等待找到满足条件的情况。通常可以通过“最终”变量来完成此操作,因此每次评估条件时都需要进入同步块(您不能在进入循环之前锁定并解锁它,因为在这种情况下,另一个进程将无法更改变量)。所以你需要像这样的东西:

boolean exit = false;
while( exit==false ){
    synchronize(var){
        if(var = CONDITIONS_MEET)
            exit=true;
    }
}
//operations...

}

但是,使用等待-通知机制会更加高效(在语言层面上),不会浪费CPU周期,并且使用良好的实践方法!

结论:你正在使自己的生活变得复杂,而这样做很可能不会更快(非常非常非常不可能)。


最终结论:只有在您了解程序将运行的操作系统和环境的具体细节,并且处于非常简单的场景中时,才可以考虑使用忙等待方法。
我希望这回答了您的问题。如果有不清楚的地方,请不要犹豫,随时问我。

0

忙等待比普通的等待通知更快。

  1. 为什么要等待?因为生产者或其他线程会执行一些工作,然后设置条件(或通知),以便您可以实际上退出繁忙/等待循环。 现在假设如果您的生产者正在执行一些重负载任务,那么通过执行繁忙等待来占用其CPU周期(主要适用于单处理器系统),这将反过来可能使您的系统整体变慢。

  2. 那么现在应该何时使用繁忙等待呢? 正如Claudio所说,它主要用于低延迟系统。 但仍不应盲目使用。当您的生产者以稳定速率生产物品时,请使用繁忙等待。 如果您的生产者以可变速率(通常由Poisson分布演示)生产物品,则应该使用wait notify。

  3. 通常,在高吞吐量和低延迟系统中,最好的权衡是进行一段时间的繁忙等待,然后进入wait()状态。 如果您的系统需要超低延迟,则可以进行许多优化之一是Busy-Wait。 但是,不应该每个线程都在繁忙等待。确保只有一些消费者约N/2个消费者正在繁忙等待,其中N是您系统中的核心数。浪费CPU周期可能会影响整体性能和响应能力。 供您参考:即使是普通的ReentrantLock及其变体也适用这些策略。即当线程调用lock.lock()时,在进入队列并等待锁被释放之前,它尝试两次获取锁。对于低延迟系统,甚至可以为特定场景定义自己的锁,在进入队列之前尝试多达10次(它们将是所谓的自旋锁变体)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接