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