Java 中啥是快速、等待通知或忙等待?

Posted

技术标签:

【中文标题】Java 中啥是快速、等待通知或忙等待?【英文标题】:What is fast, wait notify or busy wait in Java?Java 中什么是快速、等待通知或忙等待? 【发布时间】:2014-07-25 05:43:12 【问题描述】:

我知道使用忙等待不是一个好的编程习惯,最好尽可能使用同步对象(等待通知)。但我想知道是否准备好牺牲 cpu 周期,那么忙等待会更快还是等待通知?

我假设等待通知将涉及对同步对象的内在锁定,并且信号可能来自内核以唤醒线程,这使得这种方法比忙碌等待慢得多,在这种方法中,人们可以连续检查一个条件直到满意为止。一旦满足此条件(例如布尔值 == true),线程可能会退出忙等待。据我了解,我觉得忙等待应该更快。

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

【问题讨论】:

我可能错了,但是如果强制CPU循环,会不会比允许CPU空闲时慢? 我无法相信在现代系统中,你不是在写“金属”,而是在操作系统上运行并通过驱动程序,谁知道有多少操作系统代码等待 IO,那忙等待会比阻塞快得多。但是,嘿,如果有疑问,请尝试两种方法并衡量结果! 写一个实验真的比写一个问题要花更长的时间吗?我怀疑你会发现一个快速的实验让你毫无疑问地确定正确的路线。 使用wait()/notify() 将是有利的,因为一旦您notify(),(其中一个)等待线程就会收到通知并开始执行。即,调用notify() 的线程将不会继续。如果您忙于等待,即使第二个线程设置了第一个线程正在等待的布尔标志,第二个线程仍会执行,直到其时间片完成然后第一个线程启动。 @其他人,如果我错了,请纠正我.. @TheLostMind notify() 不会导致调用线程挂起,也不会导致等待线程立即唤醒。相反,被通知线程仍然必须等待通知线程首先离开同步块。关于您的时间片问题:在多核系统中,线程可能真的并行运行!所以你的说法并不完全正确。 【参考方案1】:

实验表明,如果您忙于等待,那么您会比等待并通知(无论如何在我的硬件上)更快地看到标志。 (详情如下。)差异是非常非常非常非常小,因此这仅适用于非常罕见的应用程序。例如,股票交易应用程序,如果公司追求他们可以获得的任何优势(争相将他们的服务器放置在尽可能靠近交易所的地方,以便从交易所获得其网络馈送的微秒级改进等)可能会认为这种差异是值得的。我也可以想象一些科学应用。

在绝大多数应用程序中,差异实际上根本没有差异。

但是发生在 CPU 上的当然是其中一个核心硬钉:

就影响机箱上的其他进程和数据中心的功耗而言,这很糟糕。

所以:非常不情愿地使用,仅在真正重要的情况下使用。


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

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

时间以 nano秒为单位。十亿分之一秒。上面的平均增量为 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:

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));
    

【讨论】:

while 循环通常不应该在同步块中,而不是相反吗? @isnot2bad:这取决于你在做什么。通常的规则是尽可能少地同步。在这种情况下,这意味着在 waitnotify 调用附近。当然,理论上,我们从来没有真正循环(因为这需要我们的升旗代码之外的其他东西执行notify),我们只是进入while的主体一次,然后从不重复,所以对于这个测试代码来说,无论哪种方式都是一样的。 @T.J.Crowder 你是对的。另一件事是您正在修改同步块之外的共享标志。 (我知道它适用于您的情况,因为以下同步块确保 happens-before 关系,但仍然......)。顺便提一句。我稍微修改了您的代码并在循环中调用它以进行 JVM 预热,从而大大加快了繁忙等待示例的速度。 如果预期的等待时间长于线程的剩余时间片,那么旋转的任何优势都会完全消失。如果线程被换出,则首选等待/通知。 @JimMischel:是的,我可以发誓 OP 会说假设有足够的内核,但我现在看不到。我刚刚在我的四核机器上做了一个 20 秒的测试,该机器有三个虚拟机和运行在它上面的各种其他东西(Linux)。数字与我上面的半秒测试一致。但是我对时间片等的了解很少,并且其他核心没有远程加载,所以我猜线程被允许保留核心。【参考方案2】:

一个准备好在繁忙的等待中牺牲 CPU 周期,因为它更快。 忙等待是实时低延迟应用程序的示例。

有一个名为lmax disruptor 的框架是为伦敦证券交易所构建的,其中一个锁定策略是忙等待,这就是他们使用它的方式。

为了超快,最好在通知您的锁时浪费 cpu 周期来节省时间。

你对所有其他的东西都是正确的,如果你用谷歌搜索一下disruptor 并阅读他们的论文,你会得到更多的澄清。关于高性能和低延迟有太多话要说。

一个不错的博客是Mechanical Sympathy。

【讨论】:

【参考方案3】:

视情况而定。有几种情况:

场景 A

如果在'busy-wait'中等待的是硬件操作(例如,从硬盘读取一个扇区到内存):

1) 硬件将执行操作。

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

3) 操作将暂停实际进程(您的忙等待进程),保存任何 CPU 寄存器的实际值,中断将在其处理中覆盖。

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

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

6) 您的流程将继续其流程。就在下一次迭代中,它将调用循环的 conthe 条件。例如,如果忙等待是:

while( !fileReady() )
    ...

fileReady() 方法将是一种在内部检查是否设置了具体标志(在 4 中修改的标志)的方法。 7)所以就在下一次迭代中,循环将进入并执行操作。

请记住,如果有另一个进程正在运行(操作系统进程、其他程序),它们会将您的进程置于进程尾部。此外,操作系统可以决定,鉴于您的进程已经使用了他可以使用的所有 CPU 周期(它花费了它的时间片),它的优先级将低于其他进入睡眠状态(而不是使用忙等待方法)的进程。需要等待某个条件。

结论。如果没有其他外部进程在同一个内核/CPU 中运行(非常不可能),则速度会更快。


场景 B

另一方面,如果busy-method正在等待另一个进程结束(或将任何变量设置为某个值,busy-wait会变慢。

1)busy-method 将在 CPU 中运行。由于其他进程没有运行,条件不能改变,所以busy-method会一直运行,直到CPU决定把CPU时间给其他进程。

2) 另一个进程将运行。如果这个过程花费的时间没有达到busy-wait过程需要的条件,则执行1),否则继续执行3)

3) 另一个(不是忙等待)进程仍会运行一段时间,直到 cpu 决定新的进程更改。

4)busy-method 会再次运行,但是现在条件已经满足,所以操作已经完成了。

结论:它比较慢,我们也拖慢了整个过程。


场景 C

如果我们有与 B 相同的场景,但有多个内核(每个内核中有一个进程)怎么办?

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

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

boolean exit = false;
while( exit==false )
    synchronize(var)
        if(var = CONDITIONS_MEET)
            exit=true;
    

//operations...

¡¡ 但是等待通知会做一些类似的事情,并且更有效(在语言级别),不会浪费 CPU 周期,并使用良好的规则!!

结论:您正在使您的生活复杂化,而这不太可能会更快(非常非常不可能)。


最终结论:只有在非常非常简单的场景中,知道操作系统的具体细节和程序运行的环境,才可以考虑busy-wait方法.

我希望这将回答您的问题。如果有不清楚的地方,请不要犹豫。

【讨论】:

【参考方案4】:

忙碌等待比正常等待通知更快。

    但是为什么要等呢?因为一个生产者或其他线程会做一些工作,然后设置一个条件(或通知),这样你就可以真正摆脱忙/等待循环。 现在假设如果您的 Producer 正在执行一些繁重的任务,那么您实际上会通过忙等待来消耗它的 CPU 周期(主要是在单处理器系统中),这反过来可能会使您的系统整体变慢。

    所以现在应该使用忙等待。 正如克劳迪奥所说,它主要用于低延迟系统。 但仍不能盲目使用。制作人时使用忙等待 正在以稳定的速度生产。 如果您的生产者以可变速率生产项目(通常由泊松分布证明),那么您可能应该使用等待通知。

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

【讨论】:

以上是关于Java 中啥是快速、等待通知或忙等待?的主要内容,如果未能解决你的问题,请参考以下文章

Java中啥是接口回调?

二 Java利用等待/通知机制实现一个线程池

等待/通知的奇怪java行为

Java并发编程系列21 | Condition-Lock的等待通知

通知()/等待()的Java问题

线程协作-等待与通知