【问题标题】:Java wait()/join(): Why does this not deadlock?Java wait()/join():为什么这不会死锁?
【发布时间】:2011-08-30 15:52:49
【问题描述】:

给定以下 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(),它被声明为'synchronized',这样它就可以调用wait()并安全地读取'mustShutdown';在 wait() 调用期间,锁被释放并在返回时重新获取,如 wait() 文档中所述。一秒钟后,主线程执行shutdown(),它再次同步,以便在另一个线程读取它的同时不访问mustShutdown。然后它通过 notify() 唤醒另一个线程并通过 join() 等待其完成。

但在我看来,其他线程不可能从 wait() 中返回,因为它需要在返回之前重新获取线程对象上的锁。它不能这样做,因为shutdown() 在join() 中仍然持有锁。为什么它仍然可以正常工作并正常退出?

【问题讨论】:

  • 因为这样的副作用直接扩展 Thread 是不受欢迎的。您应该实现一个用线程包装的 Runnable。

标签: java multithreading join wait notify


【解决方案1】:

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

见下面join()的代码:

public final synchronized void join(long millis) 
    throws InterruptedException {
    ....
    if (millis == 0) {
       while (isAlive()) {
         wait(0);  //ends up releasing lock
       }
    }
    ....
}

你的代码看到这个而不是一般看到的原因::你的代码看到这个而不是一般没有观察到的原因,是因为 join() 方法 waits() on Thread 对象 本身并因此放弃对 Thread 对象本身的锁定,并且由于您的 run() 方法也在同一个 Thread 对象上同步,因此您会看到这种意想不到的情况。

【讨论】:

  • 这确实解释了它。但正如 Paŭlo Ebermann 指出的那样,文档没有说明这一点。我可以依靠 join() 释放锁吗?
  • 绝对是的。自从我进入青春期以来,代码没有改变。不用担心。
  • @jlh 再想一想,Peter Lawrey 的评论也很有意义......“实现一个用线程包装的 Runnable”
  • 我去看看,谢谢。 “扩展线程实现可运行的 java” 非常适合谷歌搜索。
  • 我有一个疑问。在shutdown方法调用notify的那一刻,锁会被释放,所以不存在join保持锁的问题,因为只有在再次获得锁时才会执行。我是正确的还是遗漏了什么?
【解决方案2】:

Thread.join 的实现使用了等待,它释放了它的锁,这就是它不阻止其他线程获取锁的原因。

以下是本示例中发生的事情的分步说明:

在 main 方法中启动 MyThread 线程会导致一个新线程执行 MyThread run 方法。主线程休眠一秒钟,给新线程足够的时间来启动并获取 MyThread 对象上的锁。

然后新线程可以进入等待方法并释放它的锁。此时新线程进入休眠状态,它不会再次尝试获取锁,直到它被唤醒。线程还没有从等待方法返回

此时主线程从睡眠中唤醒并在 MyThread 对象上调用 shutdown。获取锁没有问题,因为新线程一旦开始等待就会释放它。主线程现在调用通知。进入join方法,主线程检查新线程是否还活着,然后等待,释放锁。

一旦主线程释放锁,通知就会发生。由于新线程在主线程调用通知时处于等待锁定的状态,因此新线程接收到通知并唤醒。它可以获取锁,离开wait方法,执行完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;
    }
}
}

【讨论】:

    【解决方案3】:

    补充其他答案:我没有看到在 API 文档中提到 join() 释放任何锁,所以这种行为实际上是特定于实现的。

    从中学习:

    • 不要继承 Thread,而是使用传递给线程对象的 Runnable 实现。
    • 不要同步/等待/通知您不“拥有”的对象,例如你不知道还有谁可以同步/等待/通知它。

    【讨论】:

    • 在这种特定情况下,实现 Runnable 而不是子类化 Thread 对我来说似乎更复杂......而且只有 MyThread 使用过“同步”、wait()、join() 和 notify()。主类从不使用它。 (尽管主线程确实如此。)所以我不确定你的第二个要点。
    • 重点是join 内部使用了wait(),这是你可能没有想到的。 IE。 MyThread 对象的监视器有两个用途:同步您自己的运行/关闭周期,以及同步内部线程管理。
    • 从 Java 7 开始,join(long) 的 API 规范说:“这个实现使用 this.wait 调用的循环,以 this.isAlive 为条件。当线程终止时,调用 this.notifyAll 方法."。由于涉及等待,因此也涉及锁定。 Paulo 的回答一定是基于 Java 6 API,因为当时 Java 7 刚刚发布。
    猜你喜欢
    • 1970-01-01
    • 2019-12-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-20
    • 1970-01-01
    相关资源
    最近更新 更多