2010-07-22 42 views
6

Joshua Bloch的“Effective Java”,项目51不是依赖于线程调度器,也不是在可运行状态下不必要地保持线程。引用文字:Java并发JDK 1.6:繁忙等待比信令更好吗?有效的Java#51

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

然后继续显示一个忙碌的等待与正确使用信号的微基准。在这本书中,忙碌的等待每秒钟进行17次,而等待/通知版本则每秒进行23,000次往返。但是,当我在JDK 1.6上尝试相同的基准测试时,我看到的恰恰相反 - 忙等待时间为760K往返/秒,而等待/通知版本则为53.3K往返/秒 - 即等待/通知应该已经快了1400倍,但实际上慢了13倍?

我知道忙碌的等待并不好,信号仍然更好 - 在繁忙的等待版本上cpu利用率为〜50%,而在wait/notify版本上cpu利用率保持在〜30% - 但有没有解释号码?

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

UPDATE:Source below。要运行繁忙的工作台,将PingPongQueue的基类更改为BusyWorkQueue import java.util.LinkedList; import 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(); 
    } 
} 

回答

6

在您的测试,队列不断获得新的项目,所以忙等待几乎不做实际的等待。

如果队列每1ms获得一个新项目,您可以看到busy-wait会花费大部分时间刻录CPU。它会减慢应用程序的其他部分。

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

+0

我认为就是这样。刚试过了。在将物品放入另一个队列之前先引入1ms睡眠,两次运行几乎完全相同 - 大约400次往返/秒。正如所料,繁忙的等待消耗了3倍以上的CPU。谢谢! – Raghu 2010-07-22 18:39:22

3

是的,忙等待会更迅速地响应并执行多个圈,但我认为这一点是,它把整个系统上的不成比例的负荷较重。

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

我认为你观察到的差异可能是太阳重新优化编译器,而不是人们应该做什么。 Sun一直这么做。这本书中的原始基准可能是由于某些调度程序错误,即Sun固定的 - 这个比例肯定听起来不对。

+0

那么,这本书似乎暗示你总是会用忙碌的等待来支付罚款 - 即使你只有几条线索。此外,引用的数字也表明了这一点。我看到提高的利用率,并明确了解如果您有足够的其他线程会发生什么。所以 - 仍然感到困惑。 – Raghu 2010-07-22 17:40:48

1

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

但是原子整数(AtomicInteger,AtomicIntegerArray ...)比同步Integer或int []更好,即使线程也处于繁忙等待状态。

使用java.util.concurrent包,并在你的情况往往ConcurrentLinkedQueueas尽可能

0

忙碌的等待并不总是一件坏事。使用Java同步原语的“正确”(以低级别)方式 - 执行通常很重要的簿记开销,这是执行通用机制所必需的,在大多数情况下表现相当好。另一方面,繁忙的等待是非常轻量级的,并且在某些情况下,对于一刀切的同步可以是一个相当大的改进。虽然仅基于繁忙等待的同步在任何一般环境中都是不可能的,但它非常有用。不仅对于Java而言,例如,自旋锁(基于繁忙等待的锁的花式名称)广泛用于数据库服务器。实际上,如果你浏览一下java.util.concurrent包的源代码,你会发现很多地方包含“棘手”的,看起来很脆弱的代码。我发现SynchronousQueue是一个很好的例子(你可以看一下JDK发行版或here的源代码,OpenJDK和Oracle似乎都使用相同的实现)。忙等待被用作优化 - 在一定量的“旋转”之后,线程进入适当的“睡眠”。除此之外,它还具有一些其他细节 - 不稳定的捎带,依赖于CPU数量的自旋阈值等等。它真的......照亮了它,因为它展示了实现有效的低级并发所需要的东西。更好的是,代码本身是非常干净的,有充分的文件记录和一般的高质量。