ReentrantLock条件signallAll如何工作?

问题描述

我试图了解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(),多个线程在其停放的点(即,在关键部分内部)恢复执行,所以什么也不会中断?以及何时/何地应该使用每种最佳做法

解决方法

简短答案:

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()vs signalAll():

当第一个线程获得锁的持有权时,它将调用init()方法,启动线程进行初始化,然后在其调用awaitUninterruptible()时释放该锁。同时,初始化线程尝试获取该锁,但是直到调用awaitUninterruptible()时它才会获取该锁。

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

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