【问题标题】:Is it best-practise to Condition#signal eagerly?急切地调节#signal是最佳实践吗?
【发布时间】:2015-08-04 08:58:36
【问题描述】:

Condition JavaDoc 有以下代码示例:

class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }

如果我实现了BoundedBuffer#take,我只会在count == (items.length - 2) 时才调用notFull.signal(),以避免在必要时向其他线程发出信号。我还注意到ArrayBlockingQueue#removeAt 正在热切地呼叫notFull.signal();

问题:我的支票会引入错误吗?在条件为真时急切地发出信号是 Java 并发编程的最佳实践吗?我认为它会降低死锁的风险。它对性能有影响吗?

【问题讨论】:

  • 这似乎没有什么不同,因为从另一个受同一个锁保护的块看时,lock()unlock() 之间的一切似乎都是原子发生的。当锁被争用时,它会影响调度吗?这样看起来也更具可读性。

标签: java multithreading concurrency java.util.concurrent


【解决方案1】:

是的。您的检查引入了一个错误。用一个病态的例子来说明是相当容易的。

假设count = items.length:

Thread1: put(o1); // count == items.length. Waiting on notFull.
Thread2: put(o2); // Waiting on notFull.
Thread3: put(o3); // Waiting on notFull.
Thread4: take();  // count -> items.length - 1
                  // There's space in the buffer, but we never signalled notFull.
                  // Thread1, Thread2, Thread3 will still be waiting to put.
         take();  // count -> items.length - 2, signal notFull!
                  // But what happens if Thread4 manages to keep the lock?
         take();  // count -> items.length - 3
         ...
         take();  // count -> 0
Thread1: // Wakes up from Thread4's signal.
         put(o1); // count -> 1
Thread2: put(o2); // Never signalled, still waiting on notFull.
Thread3: put(o3); // Never signalled, still waiting on notFull.

使用signalAll 可以缓解这种情况,但您仍然会遇到一些问题:

  1. 由于即使只打开了一个空间也会唤醒每个线程,因此会出现更多的锁争用。
  2. 当您的缓冲区中正好有一个空间打开时,您仍然会有线程等待put。根据实施/文档,我想这可能没问题,但在大多数情况下仍然会相当令人惊讶。

【讨论】:

    【解决方案2】:

    我的支票会引入错误吗?

    是的。 条件 takeptr == (items.length - 1) 与填充底层缓冲区无关。看来缓冲区是以“循环”的方式实现的。

    在示例代码中,每次成功的 take 都会使缓冲区为 put 做好准备,因此您应该每次都发出信号。如果没有,线程将等待“put”,尽管缓冲区中有可用空间...

    Java 并发编程的最佳实践是在条件为真时急切地发出信号吗?

    据我所知,有一条一般的经验法则可以在每个可能使条件变为真的事件之后发出信号。但是正确的条件检查通常是通过等待线程来完成的。

    您的示例场景非常简单(调用“signal”的代码确保等待“notFull”条件的任何线程都会满足“notFull”条件),但通常也使用 signalAll 代替信号,作为信号方法只向一个线程发出信号(实际上可能不满足条件),这可能会导致死锁。

    【讨论】:

    • 我真的很抱歉,我的意思是count == (items.length - 2)。我更新了我的问题。
    猜你喜欢
    • 2019-05-03
    • 2013-12-29
    • 2011-05-18
    • 1970-01-01
    • 1970-01-01
    • 2010-09-23
    • 2018-04-22
    • 1970-01-01
    • 2010-10-11
    相关资源
    最近更新 更多