【问题标题】:Java Concurrency JDK 1.6: Busy wait does better than signalling? Effective Java #51Java 并发 JDK 1.6:忙等待比发信号更好?有效的 Java #51
【发布时间】:2010-07-22 17:24:11
【问题描述】:

Joshua Bloch 的“Effective Java”,第 51 项不是关于依赖于线程调度程序以及不让线程不必要地处于可运行状态。引用文字:

减少可运行线程数量的主要技术是让每个线程 做少量工作,然后使用 Object.wait 或等待一些条件 使用 Thread.sleep 的时间。线程不应该忙着等待,重复检查数据 等待某事发生的结构。除了使程序容易受到 调度器的变幻莫测,忙等待会大大增加处理器的负载, 减少其他进程可以在同一台机器上完成的有用工作量。

然后继续显示繁忙等待与正确使用信号的微基准。在本书中,忙等待每秒执行 17 次往返,而等待/通知版本每秒执行 23,000 次往返。

但是,当我在 JDK 1.6 上尝试相同的基准测试时,我看到的正好相反 - 繁忙等待每秒 760K 往返,而等待/通知版本每秒 53.3K 往返 - 也就是说,等待/通知应该有速度快了约 1400 倍,但结果却慢了约 13 倍?

我知道繁忙的等待并不好,并且信号仍然更好 - 繁忙等待版本的 CPU 利用率约为 50%,而等待/通知版本的 CPU 利用率保持在 ~30% - 但有什么可以解释数字?

如果有帮助,我将在 Win 7 x64(核心 i5)上运行 JDK1.6(32 位)。

更新:来源如下。要运行繁忙的工作台,请将 PingPongQueue 的基类更改为 BusyWorkQueue 导入 java.util.LinkedList; 导入 java.util.List;

abstract class SignalWorkQueue { 
    private final List queue = new LinkedList(); 
    private boolean stopped = false; 

    protected SignalWorkQueue() { new WorkerThread().start(); } 

    public final void enqueue(Object workItem) { 
        synchronized (queue) { 
            queue.add(workItem); 
            queue.notify(); 
        } 
    } 

    public final void stop()  { 
        synchronized (queue) { 
            stopped = true; 
            queue.notify(); 
        } 
    } 
    protected abstract void processItem(Object workItem) 
        throws InterruptedException; 
    private class WorkerThread extends Thread { 
        public void run() { 
            while (true) {  // Main loop 
                Object workItem = null; 
                synchronized (queue) { 
                    try { 
                        while (queue.isEmpty() && !stopped) 
                            queue.wait(); 
                    } catch (InterruptedException e) { 
                        return; 
                    } 
                    if (stopped) 
                        return; 
                    workItem = queue.remove(0); 
                } 
                try { 
                    processItem(workItem); // No lock held 
                } catch (InterruptedException e) { 
                    return; 
                } 
            } 
        } 
    } 
}

// HORRIBLE PROGRAM - uses busy-wait instead of Object.wait! 
abstract class BusyWorkQueue {
    private final List queue = new LinkedList();
    private boolean stopped = false;

    protected BusyWorkQueue() {
        new WorkerThread().start();
    }

    public final void enqueue(Object workItem) {
        synchronized (queue) {
            queue.add(workItem);
        }
    }

    public final void stop() {
        synchronized (queue) {
            stopped = true;
        }
    }

    protected abstract void processItem(Object workItem)
            throws InterruptedException;

    private class WorkerThread extends Thread {
        public void run() {
            final Object QUEUE_IS_EMPTY = new Object();
            while (true) { // Main loop
                Object workItem = QUEUE_IS_EMPTY;
                synchronized (queue) {
                    if (stopped)
                        return;
                    if (!queue.isEmpty())
                        workItem = queue.remove(0);
                }

                if (workItem != QUEUE_IS_EMPTY) {
                    try {
                        processItem(workItem);
                    } catch (InterruptedException e) {
                        return;
                    }
                }
            }
        }
    }
}

class PingPongQueue extends SignalWorkQueue {
    volatile int count = 0;

    protected void processItem(final Object sender) {
        count++;
        SignalWorkQueue recipient = (SignalWorkQueue) sender;
        recipient.enqueue(this);
    }
}

public class WaitQueuePerf {
    public static void main(String[] args) {
        PingPongQueue q1 = new PingPongQueue();
        PingPongQueue q2 = new PingPongQueue();
        q1.enqueue(q2); // Kick-start the system

        // Give the system 10 seconds to warm up
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
        }

        // Measure the number of round trips in 10 seconds
        int count = q1.count;
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
        }
        System.out.println(q1.count - count);

        q1.stop();
        q2.stop();
    }
}

【问题讨论】:

    标签: java multithreading concurrency synchronization


    【解决方案1】:

    在您的测试中,队列不断获取新项目,因此忙等待几乎没有实际等待。

    如果队列每 1 毫秒获得一个新项目,您可以看到忙等待将花费大部分时间来白白消耗 CPU。它会减慢应用程序的其他部分。

    所以这取决于。如果您忙于等待用户输入,那肯定是错误的;而像 AtomicInteger 这样的无锁数据结构中的忙等待绝对是好的。

    【讨论】:

    • 我想就是这样。刚试了一下。在将项目放入另一个队列之前引入了 1 毫秒的睡眠,两次运行几乎相同 - 大约 400 次往返/秒。正如预期的那样,忙等待占用了 3 倍的 CPU。谢谢!
    【解决方案2】:

    是的,忙等待会更快地响应并执行更多循环,但我认为关键在于它会给整个系统带来不成比例的更重负载。

    尝试运行 1000 个繁忙的等待线程与 1000 个等待/通知线程并检查您的总吞吐量。

    我认为您观察到的差异可能是 sun 针对人们所做的事情而不是人们应该做的事情重新优化编译器。 Sun 一直在这样做。书中最初的基准甚至可能是由于 Sun 修复的一些调度程序错误 - 以这个比率听起来肯定是错误的。

    【讨论】:

    • 好吧,这本书似乎建议你“总是”使用繁忙的等待来付出代价——即使你只有几个线程。此外,引用的数字也表明了这一点。我看到了利用率的提高,并且绝对理解如果您有足够的其他线程会发生什么。所以 - 仍然感到困惑。
    【解决方案3】:

    这取决于线程的数量和冲突的程度:如果经常发生和/或消耗很多 CPU 周期,那么忙等待是不好的。

    但是原子整数(AtomicInteger,AtomicIntegerArray ...)比同步整数或int[]要好,即使线程也执行忙等待。

    尽可能多地使用 java.util.concurrent 包和 ConcurrentLinkedQueue

    【讨论】:

      【解决方案4】:

      忙碌的等待并不总是一件坏事。 “正确”(在低级别)做事的方式——使用 Java 同步原语——带来了通常很重要的簿记开销,这是实现通用机制所必需的,在大多数情况下表现得相当好。另一方面,忙等待是非常轻量级的,在某些情况下可以比一刀切的同步有相当大的改进。虽然仅基于忙等待的同步在任何一般环境中绝对是一个禁忌,但它有时非常有用。这不仅适用于 Java - 例如,自旋锁(基于忙等待的锁的花哨名称)广泛用于数据库服务器。

      事实上,如果你浏览一下 java.util.concurrent 包的源代码,你会发现很多地方都包含“棘手”的、看似脆弱的代码。我发现SynchronousQueue 是一个很好的例子(您可以查看JDK 发行版中的源代码或here,OpenJDK 和Oracle 似乎都使用相同的实现)。忙等待被用作优化 - 在一定数量的“旋转”之后,线程进入适当的“睡眠”。除此之外,它还有一些其他的优点——不稳定的搭载、依赖于 CPU 数量的自旋阈值等。它真的……很有启发性,因为它展示了实现高效的低级并发需要什么。更棒的是,代码本身非常干净、有据可查且总体质量很高。

      【讨论】:

        猜你喜欢
        • 2015-07-04
        • 2015-08-30
        • 1970-01-01
        • 1970-01-01
        • 2011-12-08
        • 2011-10-18
        • 2021-07-04
        相关资源
        最近更新 更多