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
Теория о том, что это поток, вызывающий пробуждение, может быть правдой, но только потому, что это ускоряющее событие ложного пробуждения.