Как часто будет синхронизироваться Java с основной памятью?

У меня есть набор счетчиков, которые будут обновляться только в одном потоке.

Если я прочитаю эти значения из другого потока, и я не изменен пользователем/атомарно/синхронизирован, насколько устаревшими могут быть эти значения?

Я спрашиваю, как мне интересно, могу ли я избежать использования volatile/atomic/synchronized здесь.

В настоящее время я считаю, что я не могу делать какие-либо предположения о времени для обновления (поэтому я вынужден использовать хотя бы нестабильную). Просто хочу, чтобы я не пропустил что-то здесь.

4 ответа

Похоже, что единственный способ избежать синхронизации этих переменных - сделать следующее (похоже на то, что предложил Zan Lynx в комментариях):

  • Укажите максимальный возраст, который я готов принять. Я сделаю это "интервал обновления".
  • Каждый "интервал обновления" копирует несинхронизированные переменные счетчика в синхронизированные переменные. Это будет выполняться в потоке записи.
  • Чтение потоков может считываться только из этих синхронизированных переменных.

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


Как устаревшее значение может получить, остается полностью на усмотрение реализации - spec не предоставляет никаких гарантий. Вы будете писать код, который зависит от деталей реализации конкретной JVM и который может быть нарушен изменениями в моделях памяти или как код перезаписи JIT. Спектр, кажется, написан с намерением дать исполнителям столько веревки, сколько они захотят, если они соблюдают ограничения, налагаемые изменчивыми, окончательными, синхронизированными и т.д.


Я спрашиваю, как мне интересно, могу ли я избежать использования volatile/atomic/synchronized здесь.

На практике кеш процессора, вероятно, будет синхронизироваться с основной памятью в любом случае на регулярной основе (как часто это зависит от многих параметров), поэтому кажется, что вы сможете время от времени видеть некоторые новые значения.

Но это не так. Фактическая проблема заключается в том, что если вы не используете правильный шаблон синхронизации, компилятор может "оптимизировать" ваш код и удалить часть обновления.

Например:

class Broken {
 boolean stop = false;
 void broken() throws Exception {
 while (!stop) {
 Thread.sleep(100);
 }
 }
}

Компилятор имеет право переписать этот код как:

void broken() throws Exception {
 while (true) {
 Thread.sleep(100);
 }
}

потому что нет никаких обязательств по проверке того, может ли энергонезависимый stop изменяться при выполнении метода broken. Отметьте переменную stop как volatile и что оптимизация больше не разрешена.

Нижняя строка: если вам нужно разделить состояние, вам нужна синхронизация.


Java8 имеет новый класс LongAdder, который помогает с проблемой использования volatile в поле. Но до тех пор...

Если вы не используете volatile на вашем счетчике, то результаты непредсказуемы. Если вы используете volatile, тогда возникают проблемы с производительностью, поскольку каждая запись должна гарантировать согласованность кеш-памяти. Это огромная проблема с производительностью, когда часто написано много потоков.

Для статистики и счетчиков, которые не являются критичными для приложения, я предоставляю пользователям опцию volatile/atomic или none, у которых нет значения по умолчанию. Пока что большинство из них не используют.

licensed under cc by-sa 3.0 with attribution.