Насколько большая задержка при многопоточном доступе может быть при обновлении значения без volatile

Предыстория:

Есть объект(список объектов на самом деле), в одном из полей которого лежит HashMap. Эта HashMap довольно часто перезаписывается (не значения в ней, а ссылка на объект - т.е. утрируя myObject.mapField = new HashMap(...)) Эта мапа довольно активно читается(только) из разных потоков. При это мне НЕ важно, если какой-то поток будет использовать данные, допустим, секундной давности.

Суть вопроса:

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


Ответы (2 шт):

Автор решения: Alexander Pavlov

Если что-то закэшировано в L1/2/3 кэшах процессора, то оно там может оставаться вечно. Перечитать процессор может только если ему придётся очистить кэш для загрузки каких-то других данных, а потом ему потребуется прочитать этот map, то тогда он уже загрузит свежие данные.

Вот там в ответах даны приблизительные тайминги для чтения из кэша и из памяти

https://stackoverflow.com/questions/4087280/approximate-cost-to-access-various-caches-and-main-memory

Т.е. можешь считать, что с volatile всегда будет 60-100нс. Не так много приложений, где это критично и я на 99.99999% уверен, что твоё не из них, потому что ты задаёшь вопросы довольно начального уровня.

Для оставшихся 0.00001% можно сделать так

volatile Map volatileMap = ...

void someLongRunningThread() {
  Map localCopy = volatileMap;
  Map lastSync = System.currentTimeMillis();

  while (true) {
    if (System.currentTimeMillis() - lastSync > 1000) {
      localCopy = volatileMap;
      lastSync = System.currentTimeMillis();
    }
    // ... work with map
  }
}
→ Ссылка
Автор решения: Roman Konoval

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

Java Memory Model описывает, как будет производиться синхронизация, если точнее, то взаимосвязь видимости результатов операций в контексте, какие операции будут гарантированно видеть результаты других. То что не описано отдается на откуп реализации, т.е. компилятору и JVM. Т.е. и реализация, которая после каждой операции вставляет барьер памяти (очень неоптимальная, таких уверен нет), и та, которая вставляет барьеры в местах абсолютно необходимых (т.е. описанных в JMM), делая дополнительно переупорядочивание операций доступа к памяти (которые не нарушают JMM) будут соответствовать спецификации, но будут сильно отличаться производительностью.

Вот что говорится в спецификации:

An implementation is free to produce any code it likes, as long as all resulting executions of a program produce a result that can be predicted by the memory model.

This provides a great deal of freedom for the implementor to perform a myriad of code transformations, including the reordering of actions and removal of unnecessary synchronization.

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

В JMM не ничего, чтобы заставляло бы увидеть запись в не volatile переменную при чтении из другого потока "просто так" или "через какое-то время". Это требуется только в четко определенных случаях, если грубо то:

  1. первый поток записал в volatile, второй из нее прочитал
  2. первый поток отпустил монитор, второй захватил
  3. первый поток запустил второй
  4. первый поток завершился, второй дождался завершения первого
  5. первый поток послал interrupt, второй его получил

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

→ Ссылка