【问题标题】:ReentrantLock Condition how does signallAll() work?ReentrantLock Condition signallAll() 是如何工作的?
【发布时间】:2020-08-23 13:29:04
【问题描述】:

例如,我试图了解 signalAll() 如何不破坏关键部分

//Some Class Here is Trying to use an resource which is to to initialized in an seperate thread

ReentrantLock lock=new ReentrantLock();
Condition wait=lock.newCondition();

Resource res=new Resource(lock); //An Important Resource Which Should Be Accessed Only By One Thread At A Time

 void doStuff()
 {
  lock.lock();
 
   try
   {
    if(!res.initialized.get()) //Check If Resource Was Previously Not Initialized
    {
     res.init();//Spawns An Seperate Thread For Initialization

     wait.await();//If Resource Is Not Initialized Make Whatever Thread Which Reached This Point Wait
    } 

    res.use(); //Here Is What I Don't Understand If Let's Say 5 threads were parked in the previous statement then when signalAll() is called won't all 5 threads wake up simultaneously and resume execution at this point and use this resource at the same time and break this resource? But it dosen't turn out like that why?
    }
  }
  finally{lock.unlock();}
 }    

 private static final class Resource extends Thread
 {
  private final ReentrantLock lock;
  private final Condition init;

  private final AtomicBoolean 
  started=new AtomicBoolean(false), 
  initialized=new AtomicBoolean(false);
  
  private Resource(ReentrantLock lock)
  {
   this.lock=lock;
   this.init=lock.newCondition();
  }
  
  private void init()
  {
   if(!initialized.get())
   {
    if(!started.get())
    {
     start();
     
     started.set(true);
    }

    while(!initialized.get()){init.awaitUninterruptibly();}//In case of spurrous wakeups check repeatedlly
   }
  }

  private void use(){}//Important Stuff  

  private int get(){return 5;}
  
  @Override
  public void run()
  {
   lock.lock();
   try
   {
    TimeUnit.SECONDS.sleep(5);
    
    initialized.set(true);
    
    init.signalAll(); //This should in the above example wake up all 5 threads simultaneously and break the critical section but that does not happen how?
   }
   catch(InterruptedException ex){}
   finally{lock.unlock();}
  }
 }

仅使用 signal() 只有一个线程唤醒并在关键部分恢复执行,所以没有中断,但是使用 signalAll() 多个线程在它被停放的点恢复执行[即在关键部分内],所以没有什么中断?以及我们应该在何时/何地使用每种方法,即最佳实践

【问题讨论】:

    标签: java multithreading locking


    【解决方案1】:

    简短回答:

    await() 不仅挂起当前线程,还释放锁。 signalAll() 唤醒所有挂起的线程,但每个线程必须在 await() 调用返回之前重新获取锁。有了它,即使在调用 notifyAll() 之后,临界区也只能由在释放锁之前获得锁的线程之后的线程进入。

    长答案:

    为了更好地理解——让我们假设在 Java 中既不存在 await()、singal() 也不存在 signalAll()。您将如何等待资源的异步初始化?您的代码可能看起来像这样:

    void doStuff(Resource resource) throws InterruptedException {
    
      lock.lock();
        try {
          while (!resource.isInitialized()) {
            resource.startAsyncInit();
            lock.unlock();
            Thread.sleep(100);
            lock.lock();
        }
        doSomethingWith(resource);
      } finally {
        lock.unlock();
      }
    }
    

    但这会有以下缺点:

    每个等待初始化的线程都被挂起,一次又一次地被唤醒。 每个单线程获取锁并一次又一次地释放锁。 您执行此操作的频率越高,等待的线程越多,成本就越高 它得到了。这种忙碌的等待会消耗 CPU。如果没有明确的睡眠,代码很容易 导致 100% 的 CPU 使用率。

    await() 允许您使用基于信号的机制来替换繁忙的等待。 所有等待的线程都被挂起,并且在等待时不消耗任何 CPU。 只有偶尔发生的虚假唤醒(至少在某些系统上)可能会消耗一些 CPU。

    一般何时使用singal()、signallAll():

    唤醒线程和挂起线程不是免费的,而且会变得更昂贵 线程数。如果您有必须初始化的资源 在它可以被所有线程同时使用之前,通过调用 signalAll() 一次唤醒所有线程是有意义的。

    但是想想具有多个消费者线程和多个生产者线程的消费者/生产者模式,其中单个生产者线程仅提供一个由一个消费者线程处理的工作项。在这种情况下,生产者线程只唤醒一个消费者线程而不是所有消费者线程会更有意义。否则所有被唤醒的线程将首先竞争单个工作项,一个会赢,其他所有的都必须再次被送回休眠状态。每次生成单个工作项时都必须重复此操作。在短时间内生产大量工作项目时 你最终会失去单选的所有优势。大多数线程会一次又一次地被挂起和唤醒,它们会一次又一次地竞争一个项目,你最终会得到与上面例子几乎相同的开销,但没有明确的睡眠;-)

    signal() 与 signalAll() 在您的示例中:

    当第一个线程获得锁时,它调用 init() 方法,启动线程进行初始化,然后在调用 awaitUninterruptibly() 时释放锁。初始化线程同时尝试获取锁,但直到调用 awaitUninterruptibly() 才能获取。

    默认情况下锁定是不公平的。这意味着不能保证等待时间最长的线程首先获得锁。当实际调用 awaitUninterruptibly() 并释放锁时,其他线程可能已经尝试通过调用 lock() 方法获取锁。即使您的初始化线程首先尝试获取锁,也不能保证它会在任何其他线程之前获得锁。在初始化线程之前获得锁的所有其他线程都将能够调用 await() 方法。如果您随后仅在初始化线程中调用 singal(),则所有能够进行 await() 调用的线程将永远不会被唤醒并永远休眠。为避免这种情况,必须在您的示例中使用 singalAll() 。另一种可能性是使用“公平”锁(参见 ReentrantLock 的 JavaDoc)。使用“公平”锁,无论您调用 singal() 还是 signalAll(),都不应该有任何区别。但由于“公平”锁有相当大的开销,我建议保留不公平锁并使用 singalAll()。

    您是否遇到某些线程永远休眠的情况取决于正确的时机。因此,您可能可以在一台主机上运行此代码数百次而没有任何问题,但在其他主机上经常遇到这种情况。更糟糕的是,在具有虚假唤醒的环境中,您只会不时遇到这种情况;-)

    【讨论】:

    • 谢谢,现在说得通了,但是每个线程重新获取锁的意义何在?获得锁的第一个线程获取资源并执行,但是当他们现在尝试获取锁时刚刚唤醒的其余线程将再次进入睡眠状态,因为已获取锁。所以这相当于只允许一个线程首先唤醒,对吗? signallAll() 的好处是什么?
    • @RVISHAL 我再次查看了您的代码,并更新了我的答案。请参阅 anwser 的最后一部分。在我看来,在您的示例中使用 singal() 和 signalAll() 之间存在重要区别。
    • 是的,你是对的,signal() 只唤醒一个线程,但你误认为如果锁是公平的,signal()/signallAll() 不会产生差异,因为在我的演示中当我使用 signal() 时程序只唤醒一个线程并且即使在我使锁公平之后也从那里挂起。在结束这个问题之前,再问一个问题。线程被唤醒后如何重新获取锁?在我上面的例子中,线程在 res.use() 点恢复并且不会跳回到 doStuff() 的开头再次调用 lock() 那么这是怎么发生的呢?
    • 在 await() 返回之前重新获取锁。
    猜你喜欢
    • 2016-01-18
    • 2021-09-26
    • 2022-11-05
    • 1970-01-01
    • 2023-03-06
    • 1970-01-01
    • 1970-01-01
    • 2015-05-29
    • 1970-01-01
    相关资源
    最近更新 更多