Java wait()/join(): 为什么不会出现死锁?

11

给定以下Java代码:

public class Test {

    static private class MyThread extends Thread {
        private boolean mustShutdown = false;

        @Override
        public synchronized void run() {
            // loop and do nothing, just wait until we must shut down
            while (!mustShutdown) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    System.out.println("Exception on wait()");
                }
            }
        }

        public synchronized void shutdown() throws InterruptedException {
            // set flag for termination, notify the thread and wait for it to die
            mustShutdown = true;
            notify();
            join(); // lock still being held here, due to 'synchronized'
        }
    }

    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();

        try {
            Thread.sleep(1000);
            mt.shutdown();
        } catch (InterruptedException e) {
            System.out.println("Exception in main()");
        }
    }
}

运行此代码会等待一秒钟然后正常退出。但这对我来说是意外的,我期望会发生死锁。

我的推理如下:新创建的MyThread将执行run()方法,该方法声明为“同步”,因此它可以调用wait()方法并安全地读取“mustShutdown”;在wait()调用期间,锁被释放并在返回时重新获取,如wait()方法的文档中所述。一秒钟后,主线程执行shutdown()方法,该方法再次同步,以便不同时访问mustShutdown变量。然后通过notify()唤醒另一个线程,并通过join()等待其完成。

但是,在我看来,另一个线程永远无法从wait()中返回,因为它需要在返回之前重新获取线程对象上的锁。它无法这样做,因为shutdown()在join()内部仍然持有锁。为什么它仍然能够正常工作并正确退出呢?


1
正是因为像这样的副作用,直接扩展Thread被视为不好的做法。你应该实现一个Runnable,然后将其包装在一个Thread中。 - Peter Lawrey
3个回答

9

join()方法内部调用wait()方法,这将导致释放线程对象的锁。

请参见下面的join()代码:

public final synchronized void join(long millis) 
    throws InterruptedException {
    ....
    if (millis == 0) {
       while (isAlive()) {
         wait(0);  //ends up releasing lock
       }
    }
    ....
}
你的代码为什么会看到这个情况而一般情况下不会看到:之所以你的代码会看到这个情况而一般情况下不会看到,是因为join()方法等待线程对象本身,并因此放弃了对线程对象本身的锁定。由于你的run()方法也在同步使用相同的线程对象,所以你会看到这种意外的情况。

1
这确实解释了这个问题。但正如Paŭlo Ebermann所指出的,文档没有提到这一点。我能相信join()会释放锁吗? - jlh
1
当然可以。自从我青春期以来,代码没有改变过。不用担心。 - Suraj Chandran
@jlh 嗯,仔细想想Peter Lawrey的评论也有道理...即“实现一个可运行对象并将其包装在线程中”。 - Suraj Chandran
我会看一下,谢谢。"extend thread implement runnable java" 这个问题在谷歌上可以找到很多答案。 - jlh
我这里有一个疑问。当从关闭方法调用notify时,锁将被释放,因此join保持锁的问题就不存在了,因为只有在再次获得锁时才会执行它。我正确吗还是漏了什么? - AKS

3
Thread.join的实现使用wait,使其释放锁,这就是为什么它不会阻止其他线程获取该锁的原因。
以下是此示例中发生的步骤:
在main方法中启动MyThread线程将导致新线程执行MyThread运行方法。主线程休眠一整秒钟,给新线程足够的时间启动并获取MyThread对象上的锁。
然后新线程可以进入等待方法并释放其锁。此时新线程进入休眠状态,直到被唤醒前不会再次尝试获取锁。线程还没有从等待方法返回。
此时,主线程从睡眠中醒来,并在MyThread对象上调用shutdown。它没有问题地获取锁,因为新线程在开始等待后释放了它。主线程现在调用notify。在进入join方法时,主线程检查新线程是否仍然活着,然后等待并释放锁。
当主线程释放锁时通知发生。由于新线程在主线程调用notify时处于锁的等待集中,因此新线程接收通知并醒来。它可以获取锁,离开等待方法,并完成执行run方法,最终释放锁。
新线程的终止导致所有等待其锁的线程都收到通知。这唤醒主线程,它可以获取锁并检查新线程是否死亡,然后退出join方法并完成执行。
/**
 * Waits at most <code>millis</code> milliseconds for this thread to 
 * die. A timeout of <code>0</code> means to wait forever. 
 *
 * @param      millis   the time to wait in milliseconds.
 * @exception  InterruptedException if any thread has interrupted
 *             the current thread.  The <i>interrupted status</i> of the
 *             current thread is cleared when this exception is thrown.
 */
public final synchronized void join(long millis) 
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
    while (isAlive()) {
    wait(0);
    }
} else {
    while (isAlive()) {
    long delay = millis - now;
    if (delay <= 0) {
        break;
    }
    wait(delay);
    now = System.currentTimeMillis() - base;
    }
}
}

0
补充其他答案:我在API文档中没有看到任何关于join()释放任何锁的提及,因此这种行为实际上是特定于实现的。
从中学习:
  • 不要子类化Thread,而是使用传递给您的线程对象的Runnable实现。
  • 不要在您不“拥有”的对象上同步/等待/通知,例如您不知道谁可能会在其上同步/等待/通知的对象。

在这种特定情况下,实现Runnable而不是子类化Thread似乎对我来说更加复杂...只有MyThread使用'synchronized'、wait()、join()和notify()。主类从未使用过它。(尽管主线程使用了它。)所以我不确定你的第二个要点。 - jlh
重点是join内部使用了wait(),这可能出乎你的意料。即,MyThread对象的监视器用于两个目的:同步自己的运行/关闭周期和同步内部线程管理。 - Paŭlo Ebermann
从Java 7开始,join(long)的API规范说:“此实现使用基于this.isAlive条件的this.wait调用循环。当线程终止时,将调用this.notifyAll方法。”由于涉及到wait,因此也涉及到锁。保罗的答案必须基于Java 6 API,因为当时Java 7刚刚发布。 - Dheeru Mundluru

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