为啥锁条件等待必须持有锁

Posted

技术标签:

【中文标题】为啥锁条件等待必须持有锁【英文标题】:Why Lock condition await must hold the lock为什么锁条件等待必须持有锁 【发布时间】:2015-12-22 19:50:43 【问题描述】:

我对此表示怀疑,在 Java 语言中,我们需要先获取锁,然后才能等待满足某些条件。

例如 int java monitor lock:

synchronized(lock)
   System.out.println("before lock ...");
   lock.wait();
   System.out.println("after lock ...");

或并发工具:

Lock lock = new ReentrantLock();
Condition cond = lock.newCondition();

lock.lock();
try
     System.out.println("before condition ...");
     cond.await();
     System.out.println("after condition ...");
catch(Exception e)
     e.printStackTrace();
finally
     lock.unlock();

那么,为什么我们不能等待,而不持有锁?

其他语言有什么不同,还是只是 Java 不同?

希望你能在设计后说明原因,但不能只针对JAVA-SPEC定义。

【问题讨论】:

Java运行在虚拟机的上下文中,指定了虚拟机的行为。 所以,那是因为 SPEC? 当然是因为规范。奇怪的问题。 ***.com/q/7019745 因为这是 POSIX 要求。 JVM是基于它的。 ***.com/a/52384541/851185 【参考方案1】:

想象一下,你有一个线程可能需要等待的东西。也许你有一个队列,一个线程需要等到队列上有东西才能处理它。队列必须是线程安全的,因此它必须受到锁的保护。您可以编写以下代码:

    获取锁。 检查队列是否为空。 如果队列为空,则等待将内容放入队列。

糟糕,这行不通。我们持有队列上的锁,那么另一个线程如何在其上放置一些东西呢?让我们再试一次:

    获取锁。 检查队列是否为空。 如果队列为空,则释放锁并等待将内容放入队列。

糟糕,现在我们还有一个问题。如果在我们释放锁之后但在我们等待将某些东西放入队列之前,某些东西已放入队列中会怎样?在这种情况下,我们将等待已经发生的事情。

存在条件变量来解决这个确切的问题。他们有一个原子的“解锁并等待”操作来关闭这个窗口。

所以 await 必须持有锁,否则将无法确保您没有等待已经发生的事情。您必须持有锁以防止另一个线程与您的等待竞争。

【讨论】:

在 java 中,condition 或 object.wait 只能在 notify 或 signal 之后被唤醒。所以,你的解释是对的。我认为如果我们可以让条件的语义像这样:当等待/等待时,检查它的唤醒位(由通知/信号设置),如果是这样,就退出等待/等待(而不是保持等待)。而对于 notify/signal 线程,只需在进行 notify/signal 时设置条件唤醒位,即表示条件满足。如果这样设计,那么就不需要获取锁了吗? @Chinaxing 不,不要那样做。考虑:两个线程在条件变量上被阻塞。一个线程将一个对象放入队列,向条件变量发出信号并唤醒一个线程。一个线程将一个对象放入队列,向条件变量发出信号并唤醒另一个线程。第一个被唤醒的线程处理队列中的第一个项目。然后,在第二个线程可以执行之前,第一个被唤醒的线程处理队列中的第二个项目。现在第二个线程执行了,条件变量已经发出信号,但是队列是空的。【参考方案2】:

好吧,我们还在等什么?我们正在等待一个条件变为真。另一个线程将使条件为真,然后通知等待的线程。

在进入wait之前,我们必须检查条件是否为假;这个检查和等待必须是原子的,即在同一个锁下。否则,如果我们在条件已经为真时进入等待,我们可能永远不会醒来。

因此有必要在调用wait()之前已经获得锁

synchronized(lock)

    if(!condition)
        lock.wait();

如果wait() 自动且静默地获取锁,则很多 的错误将未被检测到。


wait() 唤醒后,我们必须再次检查条件——这里不能保证条件必须为真(有很多原因——虚假唤醒;超时、中断、多个服务员、多个条件)

synchronized(lock)

    if(!condition)
        lock.wait();
    if(!condition)   // check again
        ...

通常,如果条件仍然为假,我们会再次等待。因此典型的模式是

    while(!condition)
        lock.wait();

但也有不想再等的情况。


是否有合法的用例可以让裸等待/通知有意义?

synchronized(lock) lock.wait(); 

当然;应用程序可以由裸等待/通知组成,具有明确的行为;可以论证这是期望的行为;这是该行为的最佳实现。

但是,这不是典型的使用模式,没有理由在 API 设计中考虑它。

【讨论】:

好吧,即使是“裸等待”,我们也会遇到这样的问题:wait 只有在另一个线程调用notify 之后 wait 才会返回开始了。但是如果两个线程都不是synchronized 在同一个对象上,甚至没有排序关系。需要明确指出的是,不仅不保证条件变为真,也不保证在通知和唤醒之间条件不会再次变为假。【参考方案3】:

请参阅Condition 的文档。

条件类似于对象的等待池或等待集,它取代了对象监视器方法(等待、通知和通知所有)的使用。条件使一个线程能够暂停执行(“等待”),直到另一个线程通知某个状态条件现在可能为真。条件实例本质上绑定到锁,就像对象监视器方法需要共享对象的锁来等待或通知一样。因此,在对条件调用 await() 之前,线程必须锁定用于产生条件的 Lock 对象。当调用 await() 方法时,与条件关联的锁被释放。

【讨论】:

【参考方案4】:

如果线程只是在等待一个信号继续进行,那么还有其他机制可以做到这一点。大概有某种状态受锁保护,线程正在等待操作并满足某些条件。为了正确保护该状态,线程应该在等待条件之前和之后都有锁,因此要求获取锁是有意义的。

【讨论】:

【参考方案5】:

一个听起来合理的答案

这是一个 JVM 的东西。一个对象x 有:

一个Entry Set:线程队列尝试synchronized(x)

一个Waiting Set:一个名为x.wait()的线程队列

当你调用x.wait()时,JVM会将你当前的线程添加到Waiting Set;当您调用x.notify()/x.notifyAll() 时,JVM 会从Waiting Set 中删除一个/所有元素。

多个线程可以调用x.wait()/x.notify()/x.notifyAll()修改Waiting Set。为了保证Waiting Set线程的安全,JVM一次只接受一个线程的一个操作。

【讨论】:

【参考方案6】:

简单的答案是因为否则您将获得在 Object.wait javadoc 中指定的 IllegalMonitorStateException。在内部,Java 中的同步使用底层操作系统机制。所以它不仅仅是Java。

【讨论】:

是的,但是 OP 想知道为什么你会得到 IllegalMonitorStateException。

以上是关于为啥锁条件等待必须持有锁的主要内容,如果未能解决你的问题,请参考以下文章

MySQL 加锁和死锁解析

C语言之简单使用互斥锁条件锁实现生产者消费者模型操作

java多线程---重入锁ReentrantLock

线程同步之条件锁

如果我们在条件变量之前放置一个互斥锁,那么有多少线程会等待它呢?

redis分布式锁