Заметка про wait notify примитивы в Java
Давайте попробуем решить следующую задачу.
Есть класс Container
:
Мы хотим класть в него значения и забирать методами put()
и get()
Мы считаем ошибкой попытку положить в полный контейнер и забрать из пустого.
Создадим и запустим два потока, один кладет текущее время в контейнер, а другой читает что есть в контейнере:
Это предсказуемо падает с ошибкой:
1524892150711
Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.AssertionError
at ru.yamakarov.Container.put(WaitNotifyExample.java:31)
at ru.yamakarov.WaitNotifyExample.lambda$main$0(WaitNotifyExample.java:11)
at java.lang.Thread.run(Thread.java:748)
java.lang.AssertionError
at ru.yamakarov.Container.get(WaitNotifyExample.java:36)
at ru.yamakarov.WaitNotifyExample.lambda$main$1(WaitNotifyExample.java:18)
at java.lang.Thread.run(Thread.java:748)
Два потока никак не синхронизованы. Попробуем это сделать вот так:
И этот код какое-то время работает.
...
1524893196499
producer
consumer
1524893196499
producer
consumer
1524893196499
producer
consumer
1524893196499
producer
consumer
1524893196499
producer
Однако заканчивается все тем, что либо consumer
пропускает notify либо producer
.
Судя по документации может быть спонтанное пропысание на wait
:
As in the one argument version, interrupts and spurious wakeups are
possible...
Тогда должен вылететь ошибка на assert
.
Но у меня такого не происходило.
Получается представленное мной решение, одно из самых плохих.
Он некоторое время работает и в неизвестный момент ломается.
Хуже оно было бы, если ломалось не так быстро, скажем только при spurious wakeups.
Но я не знаю как этого добиться.
Вот другое решение с использованием флагa:
Код запуска:
Неожиданным в этом решении оказалось то, что в какой то момент вывод времени прекращается.
Я не понял, что останавливается, потому что добавление System.out.pritln()
убирает этот эффект.
А дебажить многопоточный код, вообще ад. У меня подозрение в том, что один из тредов выбирает все ресурсы и второй перестает выполняться полностью. Таким образом флаг produced
не меняется и ничего не происходит.
Thread.yield()
помогает на моей машине, но может не помочь на вашей судя по документации.
И начинает вылетать забавное исключение, после некоторого времени выполнения. По моей гипотезе, это разогрев JVM:
...
1524895907057
Exception in thread "Thread-1" java.lang.AssertionError
at ru.yamakarov.Container.get(WaitNotifyExample.java:48)
at ru.yamakarov.WaitNotifyExample.lambda$main$1(WaitNotifyExample.java:26)
at java.lang.Thread.run(Thread.java:748)
Такого же не может быть. Ведь мы сначала кладем значение а потом поднимаем флаг:
И вот может быть, так как оптимизитор может произвести перестановку этих операций с целью увеличения производительности.
Можно полечить сделав produced
volatile:
Вот тут я нашел объяснение этому.
Там что-то с кэшами и реодерингом. Так работает.
Но что если producer
будет работать гораздо медленнее consumer
?
consumer
будет бессмысленно жечь электричество, проверяя, а не появились ли данные.
Тут поможет wait()
в while
цикле:
И теперь хороший стабильный вывод:
...
1524898482460
consumer
producer
1524898482460
consumer
producer
1524898482460
consumer
producer
1524898482460
consumer
producer
1524898482460
consumer
...
Я написал вот такой Producer-Consumer на wait и notify. Рекомендую попробовать самим написать нечто подобное, но в проде лучше используйте решение на очереди. Будете лучше спать по ночам без spurious wakeups.