等待的线程是否会重新访问同步方法内的代码?

5
我正在阅读关于线程同步和tutorial中的wait / notify结构的内容。它说明:
当调用wait时,线程释放锁并暂停执行。在未来的某个时间,另一个线程将获取相同的锁并调用Object.notifyAll,通知等待该锁的所有线程已发生了重要事件。
在第二个线程释放锁后一段时间,第一个线程重新获取锁并通过从wait调用返回来恢复执行。
据我所知,如果有多个线程在第一个线程被notify唤醒时可以竞争锁,则其中任何一个线程都可以获得该对象上的锁。我的问题是,如果这个第一个线程本身重新获取锁,它是否必须从同步方法的开头重新开始(这意味着它再次执行while循环检查wait()条件之前的代码),还是仅在wait()行处暂停?
// Does the waiting thread come back here while trying to own the
// lock (competing with others)?
public synchronized notifyJoy() {
    // Some code  => Does this piece of code gets executed again then in case
    // waiting thread restarts its execution from the method after it is notified?
    while (!joy) {
        try {
            // Does the waiting thread stay here while trying to re-acquire
            // the lock?
            wait();
        } catch(InterruptedException e) {}
    }
    // Some other code
}
        

5
不。一个等待中的线程将从 wait 命令中恢复。 - Boris the Spider
@laune 我明白wait/notify主要用于信号传递,而synchronized用于锁访问。我唯一困惑的是线程在从“等待”状态更改为“阻塞”状态,然后再到“可运行”状态后,它是否会从离开的地方继续执行,还是必须返回并重新启动。@Boris似乎已经在评论中回答了这个问题。 - user3864457
2个回答

9
当执行线程完成其run方法的执行(无论是正常返回还是抛出未在该run方法中捕获的异常)时,才会退出方法。如果JVM在您的下面被杀死(使用java.lang.System.exit、用kill -9杀死Java进程等),或者该方法正在守护线程中运行而JVM正在关闭,则您的方法不会被执行。这里没有什么奇怪的事情发生。等待的线程放弃锁并进入休眠状态,但它并没有停止执行该方法。
从wait调用中唤醒的线程从未离开过任何地方;在线程等待期间,它仍然在wait方法中。在离开wait方法之前,它必须重新获取它为了开始等待而放弃的锁。然后,它需要重新测试它需要检查的任何条件,以确定是否继续等待。
这就是为什么受保护的块教程告诉您必须在循环中进行等待的原因。
wait的调用不会返回,直到另一个线程发出通知,表明可能发生了某些特殊事件-虽然不一定是该线程等待的事件:
public synchronized void guardedJoy() {
    // This guard only loops once for each special event, which may not
    // be the event we're waiting for.
    while(!joy) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Joy and efficiency have been achieved!");
}

注意:始终在测试所等待的条件的循环内调用wait。不要假设中断是针对您正在等待的特定条件的,或者该条件仍然为真。
(教程使用的措辞有误;单词“interrupt”应为“notification”。此外,不幸的是,教程所示的代码吃掉了InterruptedException而没有设置中断标志,最好让InterruptedException从此方法抛出并且根本不捕获它。)
如果线程“重新开始”,那么就不需要这个循环了;您的代码将从方法的开头开始,获取锁,并测试所等待的条件。

文档还指出,一次只能有一个线程在该块内部。但是,如果一个线程已经执行了该块的一部分并等待,而拥有锁的另一个线程也可以执行该块的一部分或全部,这是如何可能的呢?从技术上讲,虽然其中一个线程正在等待,但实际上有两个线程,因此违反了“同步方法/块内只允许一个线程”的原则。 - veritas
@veritas:你说得对,当一个方法执行其他代码时,另一个线程处于等待状态。只有一个线程拥有锁。事实上,由于它们没有锁,可能会有多个等待线程。 - Nathan Hughes
所以你的意思是文档实际上意味着只有一个线程可以“拥有”该方法的锁,而不一定是只有一个线程可以“在”该方法中,因为从技术上讲 - 正如你所说 - 多个线程可以在该方法中等待,但只有一个在执行?这仍然让我感到紧张。即使一些线程在等待并且已经失去了监视器,它们在进入wait()之前可能已经修改了一些变量,不是吗?这不是程序员的责任吗? - veritas
1
@veritas:锁不在方法上,而是在调用该方法的对象上。如果一个线程在等待之前修改了某些东西,那么它将持有锁。但是当线程从等待中醒来时,它必须再次获取锁,并检查当前状态。这就是为什么我的答案建议使用循环来检查条件,以便在线程唤醒时再次检查。 - Nathan Hughes

0

在调用wait后,线程执行直接开始。它不会从头重新开始阻塞。wait()的实现大致类似于

public void wait() {
    release_monitor();
    wait_monitor();
    acquire_monitor();
}

这与实际实现方式相去甚远,只是对幕后发生的事情的一个大致想法。每个对象都有一个关联的监视器,可以被获取和释放。一次只能有一个线程持有监视器,线程可以递归地获取监视器而不会出现问题。调用对象上的等待方法会释放监视器,允许另一个线程获取它。等待线程然后等待直到通过调用notify/notifyAll唤醒。被唤醒后,等待线程再次等待以获取对象的监视器并返回到调用代码。

示例:

private Object LOCK = new Object;
private int num = 0;
public int get() {
    synchronized( LOCK ) {
        System.out.println( "Entering get block." );

        LOCK.wait();

        return num;
    }
}

public void set( int num ) {
    synchronized( LOCK ) {
        System.out.println( "Entering set block." );

        this.num = num;

        LOCK.notify();
     }
}

每次调用get()时,

"进入获取块"只会打印一次。


你的示例容易出现“丢失通知”的问题。如果线程A在线程B调用set()之前调用get(),那么它可能会按照你想象的方式工作,但是如果线程B先调用set(),并且只调用一次,则线程A将永远等待。如果没有其他线程已经在foo.wait()调用中等待,foo.notify()调用不会产生任何效果。【阅读有关“虚假唤醒”的内容】。 - Solomon Slow
@jameslarge,是的,这个例子很糟糕,但它是为了展示块只执行一次而不是为了好的风格而编写的。 - Smith_61
请确保您在循环中等待。同一线程进行的递归获取称为可重入性。等待方法会持有一个想要获得监视器的线程队列,并像门一样运作,而不是像您获取的东西。 - user2982130

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接