Notify() ведет себя как notifyAll()

У меня есть один поток калькулятора, который вычисляет сумму числа от 1 до 50 и несколько потоков чтения, которые показывают результат после того, как поток калькулятора готов. У меня есть возможность вызвать notify() и notifyAll(), чтобы сигнализировать потокам чтения, что результат вычисления готов к отображению. В классе LINE B класса Calculator, если я вызываю метод notifyAll(), результат печатается 4 раза, как ожидалось. Но когда я заменяю LINE B только уведомлением(), я вижу, что результат напечатан 4 раза, что кажется неправильным. Я понимаю, что 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

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

UPDATE: Использование объекта в качестве монитора/блокировки вместо экземпляра Thread приводит к правильному поведению.

Обновлен класс 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;
    }
}

Ответ 1

Это не notify(), который просыпает весь ваш читатель Threads, но конец срока службы Calculator's Thread's.

Я не знал об этом поведении до сих пор, но кажется, что завершающий Thread будет всегда пробуждать все Threads, ожидающие его. Просто введите другой Thread.sleep() в конце Calculator.run(), и вы увидите.

Update

Там важная разница, которую я только что понял, прочитав ответ Джона.

Непонимание возникает во фразе "ожидание". A Thread действительно уведомит всех официантов, но это не имеет ничего общего с концепцией Thread.

На самом деле это особое поведение, которое, в частности, означает, что Thread, когда он достигнет конца своей жизни, уведомит всех официантов, ожидающих самого объекта Thread . Как уже указывал Джон, это произойдет в какой-то точке "после" Thread.exit(), следовательно, внутри JVM, таким образом, нет никакого отношения к освобождению объекта.

Заключение

Хотя это, скорее всего, причина описанной ошибки, нужно никогда полагаться на что-либо, что произойдет внутри JVM. Тот факт, что это поведение является документированным, является определенным индикатором.

Ответ 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, но эта функциональность, я не могу себе представить, будет сильно отличаться.

Ответ 3

Кроме того, что написано другими, я не анализировал код полностью, но, насколько ваш код wait идет, вы учитываете ложные пробуждения? Ваши ожидающие потоки можно разбудить не только вызовами notify/notifyAll, но и неопределенными. Вот почему вы всегда должны вызывать wait в цикле, например:

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

Подробнее см. этот.

Ответ 4

Ответ Izruo прав. Я хотел бы что-то добавить к этому. Если вы разместите sleep() вне синхронизированного блока, тогда один из потоков проснется, а другой поток будет находиться в состоянии ожидания.

Ответ 5

Вызов уведомления не запускает ни один из потоков чтения. Все, что он делает, - это выбрать Reader и удалить его из набора ожидания для калькулятора, который позволяет ему получать монитор калькулятора после того, как калькулятор его выпустил. Калькулятор продолжает работать, оставив синхронизированный блок и затем выйти.

После выхода из синхронизированного блока выбранный Reader начинает работать, распечатывает его общее количество, а также покидает синхронизированный блок и выходит.

В этот момент никто не владеет монитором, и поэтому теоретически каждый поток блокируется. Но спецификация Java Language Specification позволяет JVM разбудить заблокированные потоки, никто из них не уведомляет об этом: " Этот поток может быть удален из набора ожиданий из-за... Внутреннее действие реализации. Реализации разрешены, хотя и не рекомендуется, для выполнения" ложных пробуждений ", то есть для удаления потоков из наборов ожидания и, таким образом, для возобновления без явных инструкций для этого". Они не запускаются (они находятся в синхронизированном блоке и все равно должны получить блокировку), но они становятся доступными для запуска. Это должно быть то, что здесь происходит.

Дополнительная информация: Coder Ranch thread, DevGuli

Теория о том, что это поток, вызывающий пробуждение, может быть правдой, но только потому, что это ускоряющее событие ложного пробуждения.