【问题标题】:notify() behaving like notifyAll() [duplicate]notify() 的行为类似于 notifyAll() [重复]
【发布时间】:2017-03-01 12:33:05
【问题描述】:

我有一个 Calculator 线程来计算从 1 到 50 的数字之和,以及多个 Reader 线程在 Calculator 线程准备好后显示结果。我可以选择调用 notify() 和 notifyAll() 来向 Reader 线程发出计算结果已准备好显示的信号。在 Calculator 类的 LINE B 中,如果我调用 notifyAll() 方法,结果会按预期打印 4 次。但是当我只用 notify() 替换 LINE B 时,我仍然看到打印了 4 次的结果,这似乎不正确。据我了解,notify() 只会唤醒正在等待的线程之一,而不是全部。为什么我调用 notify 时所有线程都唤醒并打印结果?

public class ThreadWaitNotify {

    public static void main(String[] args) {
        Calculator c = new Calculator();
        Reader r = new Reader(c);
        Reader r2 = new Reader(c);
        Reader r3 = new Reader(c);
        Reader r4 = new Reader(c);

        r.start();
        r2.start();
        r3.start();
        r4.start();
        try {
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        c.start();
    }

}

阅读器类:

class Reader extends Thread {

    Calculator c;

    public Reader(Calculator c) {
        this.c = c;
    }

    @Override
    public void run() {
        synchronized (c) {
            try {
                System.out.println(Thread.currentThread().getName() + " Waiting for calculations: ");
                c.wait();    // LINE A
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " Total: " + c.getSum());
        }
    }
}

计算器类:

class Calculator extends Thread {

    private int sum = 0;

    @Override
    public void run() {
        synchronized (this) {
            for (int i = 1; i <= 50; i++) {
                sum += i;
            }
            notify();  // LINE B
        }
    }

    public int getSum() {
        return sum;
    }
}

输出:

Thread-1 Waiting for calculations: 
Thread-4 Waiting for calculations: 
Thread-3 Waiting for calculations: 
Thread-2 Waiting for calculations: 
Thread-1 Total: 1275
Thread-2 Total: 1275
Thread-3 Total: 1275
Thread-4 Total: 1275

=======================

更新: 使用对象作为监视器/锁而不是线程实例会产生正确的行为。

更新了 ThreadWaitNotify 类:

public class ThreadWaitNotify {

    public static void main(String[] args) {
        Object monitor = new Object();
        Calculator c = new Calculator(monitor);
        Reader r = new Reader(c, monitor);
        Reader r2 = new Reader(c, monitor);
        Reader r3 = new Reader(c, monitor);
        Reader r4 = new Reader(c, monitor);

        r.start();
        r2.start();
        r3.start();
        r4.start();
        try {
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        c.start();
    }

}

更新的阅读器类:

class Reader extends Thread {

    Calculator c;
    Object monitor;

    public Reader(Calculator c, Object monitor) {
        this.c = c;
        this.monitor = monitor;
    }

    @Override
    public void run() {
        synchronized (monitor) {
            try {
                System.out.println(Thread.currentThread().getName() + " Waiting for calculations: ");
                monitor.wait();   // LINE A
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " Total: " + c.getSum());
        }
    }
}

更新的计算器类:

class Calculator extends Thread {

    private int sum = 0;
    Object monitor;

    public Calculator(Object monitor) {
        this.monitor = monitor;
    }

    @Override
    public void run() {
        synchronized (monitor) {
            for (int i = 1; i <= 50; i++) {
                sum += i;
            }
            monitor.notify();       // LINE B
        }
    }

    public int getSum() {
        return sum;
    }
}

【问题讨论】:

  • 感谢您查看@OldCurmudgeon。代码已经贴在 ThreadWaitNotify 的 main 方法下。我正在创建 4 个 Reader 实例。
  • 我不会说更新版本是“正确的”,因为您在这两种情况下都依赖于未记录的行为。在一种情况下,它似乎符合您对代码应该如何工作的想法,但这是巧合。您假设 wait 仅在另一个线程调用 notify 时返回是不正确的。
  • 锁定Thread 是一种反模式。扩展 Thread 是一种反模式。
  • 在调用notify 时必须返回一次对wait 的调用,但也允许以任何感觉返回wait

标签: java multithreading


【解决方案1】:

唤醒所有读者Threads 的不是notify(),而是Calculator's Thread's 寿命的终结。

直到现在我也不知道这种行为,但似乎终止的Thread 总是会唤醒所有等待它的Threads。只需在Calculator.run() 末尾添加另一个Thread.sleep(),您就会看到。

更新

阅读John's answer,我刚刚意识到一个重要的区别。

误解在于“等待它”这个短语。 Thread 确实会通知所有服务员,但它与线程概念无关。

事实上,这是一种特殊的行为,特别是Thread,每当它的生命周期结束时,都会通知所有等待Thread的服务员对象本身强>。正如 John 已经指出的那样,这将在 Thread.exit() 之后的某个时间点发生,因此在 JVM 内部,因此与对象释放无关。

结论

虽然这很可能是所描述的错误的原因,但应该永远依赖 JVM 内部发生的任何事情。这种行为是有据可查的事实是一个明确的指标。

【讨论】:

  • 你是对的,即使你完全删除了notify(),所有线程最终都会在计算器线程上变弱
  • @Ali 不,尽管这听起来很糟糕,但我自己有这个想法并且只是测试了它,所以这里没有参考。对不起!
  • @Ali 如果你用普通的Object作为锁,你会看到notify()notifyAll()的区别
  • 它与Thread 无关,它与对象监视器有关,在这种特殊情况下是Thread 的一个实例。对象释放似乎确实会导致 虚假唤醒 或内部 notifyAll。 Java 内存模型来救援并解释可能的唤醒:docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.2
  • @bashnesnos 注意线程没有被释放。
【解决方案2】:

我原来的答案是错误的。我刚刚浏览了本机代码以了解发生了什么。

当一个线程结束时,它实际上会通知退出线程监视器上的所有等待线程。同样,这是在本机级别,并且可以更改

thread.cpp ->

void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
      ..... other code ....
      // Notify waiters on thread object. This has to be done after exit() is called
      // on the thread (if the thread is the last thread in a daemon ThreadGroup the
      // group should have the destroyed bit set before waiters are notified).
      ensure_join(this);
      assert(!this->has_pending_exception(), "ensure_join should have cleared");
     .... other code ....

这是来自 JDK 7 的源代码,但是我无法想象这个功能会有很大的不同。

【讨论】:

  • 因此在Thread.exit()中调用了这个方法。不错的收获!
  • 等一下,这是来自TreadGroup 而不是Thread 所以notifyAll() 将被调用到组而不是单个Thread
  • 其实我可能错了。将删除此答案。
  • @JohnnyAW 你是对的,我更新了我的答案。
  • 这看起来好多了:)
【解决方案3】:

除了其他人写的,我没有彻底分析代码,但是,就您的wait 代码而言,您是否考虑了虚假唤醒?您的等待线程不仅可以通过调用notify/notifyAll 来唤醒,而且还可以不确定地唤醒。这就是为什么您应该始终在循环中调用wait,如下所示:

while (!condition) { obj.wait(); }

详见this问题。

【讨论】:

    【解决方案4】:

    Izruo 的回答是对的。 我想给它加点东西。 如果将 sleep() 放在同步块之外,则其中一个线程将被唤醒,其他线程将处于等待状态。

    【讨论】:

    • 感谢您的回复@AmanGoyal。那不是代码的功能。请参阅上面的更新代码。如果您在线程类的实例上调用等待和通知,与对象类的实例相比,代码的行为会有所不同。如果您调用 notify() 而不是 notifyAll(),则使用对象实例,程序会挂起(其余线程保持等待状态)。
    • 同意。我误解了这种行为。我试图从 jConsole 中理解它。这真是一个非常好的问题。
    【解决方案5】:

    调用 notify 不会启动任何运行的 Reader 线程。它所做的只是选择一个 Reader 并将其从 Calculator 的等待集中删除,这使得它能够在 Calculator 释放它后获取 Calculator 监视器。计算器继续运行,离开同步块然后退出。

    离开同步块后,选定的 Reader 开始运行,打印其总数,同时离开同步块并退出。

    此时没有人拥有监视器,因此理论上每个线程都被阻塞。但是 Java 语言规范允许 JVM 在没有任何人调用 notify 的情况下唤醒阻塞的线程:“The thread may be removed from the wait set due to ... 实现的内部操作。虽然不鼓励实现,但允许执行“虚假唤醒”,即也就是说,从等待集中删除线程,从而在没有明确指示的情况下启用恢复。”它们不会开始运行(它们在同步块中并且仍必须获取锁),但它们确实有资格运行。这一定是这里发生的事情。

    更多信息:Coder Ranch thread, DevGuli

    线程退出导致唤醒的理论可能是正确的,但这仅仅是因为它是虚假唤醒的促成事件。

    【讨论】:

    • 不,这与“JVM实现某些东西”无关,如果你使用普通的Object作为锁,你最终会得到3个阻塞线程
    • 当我们使用 Thread 实例的等待和通知时,会发生一些特别的事情。如果我使用 Object 作为监视器并在其上调用 wait() 和 notify() 方法,则只有一个线程打印剩余线程挂起的总数,直到我停止程序。所以我不同意你的说法,即“JVM 会意识到,为了让程序有继续的希望,它需要恢复 Reader 线程之一......”
    • 我的理论是错误的——保持 Calculator 线程处于活动状态并不会改变行为。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-19
    • 1970-01-01
    • 2011-04-17
    • 2017-08-21
    • 1970-01-01
    • 2015-10-01
    相关资源
    最近更新 更多