Что такое модель памяти? Обычно это сочетание слов употребляется применительно к организации памяти на уровне аппаратного обеспечения, например, в многопроцессорной системе, где каждый процессор использует локальный кэш. Модель памяти, в частности, может определять условия, при которых значения записываемые в память одним процессором становятся видны всем остальным процессорам.
Если говорить о модели памяти Java, то это набор правил, описывающий выполнение многопоточных программ, правила, по которым потоки могут взаимодействовать друг с другом посредством основной памяти.
Есть ли подобные "модели памяти" в других языках? В большинстве популярных языков нет встроенной поддержки многопоточности, например, стандарт C++ говорит только об однопоточных программах, поэтому разработчикам многопоточных программ, использующим C++, надо надеяться, что те проблемы, которые призвана решить модель памяти Java, решаются компилятором или библиотеками, используемыми для поддержки многопоточности (и похоже что в случае с C++ проблемы с многопоточностью решены не до конца, об этом можно прочитать здесь).
Java поддерживает многопоточность на уровне языка, кроме этого язык Java изначально задумывался как мультиплатформенный и программы на Java должны корректно выполняться на различных процессорах, под управлением различных операционных систем, все эти факторы требуют набора общих правил для корректного взаимодействия виртуальной машины с программной и аппаратной средой выполнения.
В отсутствии подобных правил в многопоточной программе может возникнуть, например, следующий вопрос: если один поток записал в переменную var1 значение A, всегда ли другой поток, читая значение той же переменной var1, получит значение A? Это может быть не так по ряду причин: значение переменной может сохраняться в регистрах, а не в основной памяти; значение переменной может сохраняться в локальном кэше процессора и эти изменения не всегда доступны потокам, исполняющимся на других процессорах; при записи в основную память процессор может использовать буфер записи и запись в память из буфера может откладываться. Кроме этого всё усложяет оптимизирующий компилятор, который может переупорядочить операции, если это не меняет семантики программы. В этом случае без правильной синхронизации с точки зрения потока T1 действия, выполняемые потоком T2, могут происходить в порядке, не соответствующем порядку, записанном в программе, и в результате работы неправильно синхронизированной программы могут быть получены совершенно неожиданные результаты.
Надо отметить, что подобные проблемы могут возникнуть только в многопоточной программе и только если потоки работают с общими данными. Если поток работает с локальными данными, то в этом случае компилятор и процессор могут безопасно использовать весь набор оптимизаций—операции могут выполняться в любом порядке (если это не меняет логику программы) и содержимое кэш памяти не обязательно должно быть синхронизировано перед каждой операцией чтения и после каждой операции записи, и эти оптимизации, естественно, увеличивают скорость выполнение программы. В большинстве многопоточных програмах различные потоки большую часть времени выполняют действия, не связанные друг с другом, и взаимодействие требуется только в определённые моменты времени, поэтому с точки зрения скорости выполнения было бы слишком дорого ограничивать возможные оптимизации и постоянно синхронизировать память потока с основной памятью, в связи с этим разработчик многопоточной прораммы должен позаботиться о корректной организации межпоточного взаимодействия.
Формально модель памяти определяет набор действий межпоточного взаимодействия (эти действия включают в себя, в частности,
чтение и запись переменной, захват и освобождений монитора, чтение и запись volatile пере��енной,
запуск нового потока) а также модель памяти определяет отношение между этими действиями, название которого можно перевести как
'происходит раньше' (happens before). Чтобы поток, выполняющий действие B, гарантированно видел результат
действия A (независимо от того в одном или разных потоках эти действия происходят), между действиями A и
B должно существовать отношение 'происходит раньше', если этого отношения нет, то поток может не видеть
результат действия A и эти действия могут исполняться в произвольном порядке.
Существует несколько основных правил для отношения 'происходит раньше':
- в одном потоке любое действие происходит раньше любого действия, указанного в программе позже (т.е. с точки зрения потока его действия выполняются в порядке, указанном в программе)
- освобождение монитора происходит раньше, чем последующий захват того же монитора
-
запись в
volatileпеременную происходит раньше последующего чтения той же переменной -
вызов метода
start()для запуска нового потока происходит раньше любого действия в запущенном потоке - если действие A происходит раньше действия B и действие B проиcходит раньше действия C, то A происходит раньше С
Приведённый здесь список не полный и более подробно модель памяти описывает JSR 133, конечно, для написания программ не требуется досконально разбираться в этой спецификации, её цель—формально описать семантику языка Java, касающуюся многопоточности. Можно выделить несколько основных областей, имеющих отношение к модели памяти:
Видимость (visibility): в каком случае поток видит изменения переменных, сделанные другими потоками?
К вопросу видимости имеют отношение следующие ключевые слов языка Java: synchronized, volatile, final.
С точки зрения языка Java все переменные (за исключением локальных переменных, объявленных внутри метода) хранятся в главной
памяти, которая доcтупна всем потокам, кроме этого, каждый поток имеет локальную—рабочую—память, где он хранит копии
переменных, с которыми он работает, и при выполнении программы поток работает только с этими копиями. Надо отметить, что
это описание не требование к реализации, а всего лишь модель, которая объясняет поведение программы, так, в качестве
локальной памяти не обязательно выступает кэш память, это могут быть регистры процессора или потоки могут вообще не
иметь локальной памяти.
При входе в synchronized метод или блок поток обновляет содержимое локальной памяти, а при выходе
из synchronized метода или блока поток записывает изменения, сделанные в локальной памяти, в главную.
Такое поведение synchronized методов и блоков следует из правил для отношения 'происходит раньше':
так как все операции с памятью происходят раньше освобождения монитора и освобождение монитора происходит раньше
захвата монитора, то все операции с памятью, которые были сделаны потоком до выхода из synchronized блока должны
быть видны любому потоку, который входит в synchronized блок для того же самого монитора.
Очень важно, что это правило работает только в том случае, если потоки синхронизируются, используя один и тот же монитор! Так, операция
synchronized(new Object()){}
может не провоцировать синхронизацию локальной памяти потока с основной, так как в этом случае компилятор знает, что
ни один поток не может испОЛЬЗОВАТЬ этот же монитор и может просто удалить эту операцию.
Что касается volatile переменных, то запись volatile переменных производится в основную память, минуя локальную.
и чтение volatile переменной производится также из основной памяти, то есть значение volatile переменной
не может сохраняться в регистрах или локальной памяти потока, и операция чтения этой переменной гарантированно вернёт
последнее записанное в неё значение.
Также модель памяти определяет дополнительную семантку ключевого слова final, имеющую отношение к видимости
(JSR 133, часть 9): после того как объект был корректно создан, любой поток может видеть значения его final полей
без дополнительной синхронизации. 'Корректно создан' означает, что ссылка на создающийся объект не должна
использоваться до тех пор, пока не завершился конструктор объекта. Наличие такой семантики для ключевого слова final
позволяет создание неизменяемых (immutable) объектов, содержащих только final поля, такие объекты могут свободно
передаваться между потоками без обеспечения синхронизации при передаче.
Есть одна проблема, связанная с final полями: реализация разрешает менять значения final полей после
создания объекта
(это может быть сделано, например, с использованием механизма
reflection).
Если значение final поля—константа, чьё значение известно на момент компиляции, изменения final
поля могут не иметь эффекта, так-как обращения к этой final переменной могли быть заменены компилятором на константу.
Также спецификация разрешает другие оптимизации,
связанные с final полями, например, операции чтения final переменной могут быть переупорядочены с операциями,
которые потенциально могут изменить final переменную. Так что рекомендуется изменять final поля объекта
только внутри конструктора, в противном случае поведение не специфицировано.
Другая важная облать, относящаяся к модели памяти—это порядок операции (ordering): при каких условиях последовательность
операций, выполняемых одним потоком, видна другому потоку выполняемой в том же порядке?
Этот вопрос регулируется
набором правил для отношения 'происходит раньше', все эти правила довольно очевидны (мне кажется естественным что, например,
действия в новом потоке выполняются только после того как был вызван метод start()), и на мой взгляд у этих правил есть
только одно следствие, касающееся порядка операций, используемое на практике: операции чтения и записи volatile переменных не могут
быть переупорядочены с операциями чтения и записи других volatile и не-volatile переменных.
Это следствие делает возможным использование volatile переменной как флага, сигнализирующем об окончании
какого-либо действия, например:
int[] array;
volatile boolean arrayCreated;
// Поток A
array = new int[100];
arrayCreated = true;
// Поток B
while (!arrayCreated)
Thread.yield();
array[0] = 1;
Если бы переменная arrayCreated была объявлена не volatile, то операция arrayCreated = true могла быть
выполнена раньше, чем array=new int[100] и значение arrayCreated = true не означало бы, что переменная
array инициализирована и в этом случае поток B мог бы столкнуться с неожиданными результатами.
Правила, касающиеся порядка выполнения операций, гарантируют упорядоченность операций для конкретого набора случаев (таких как, например, захват и освобождение монитора), во всех остальных случаях оставляя компилятору и процессору полную свободу для оптимизаций.
Несколько ссылок по теме:
Семён Бойков
опубликовал vmrobot ( ноя 20 2006, 07:21:12 PM MSK ) Permalink Комментарии [7]

опубликовал Saw Ноябрь 20, 2006 at 08:14 PM MSK #
опубликовал denis_ka Ноябрь 27, 2006 at 12:31 PM MSK #
чишите паще!!
опубликовал П.Сорокин Август 08, 2007 at 12:27 PM MSD #
Несколько коротковато, пожалуй, но старт для начала разбирательств по данной теме неплохой...
Спасибо...
опубликовал konstb Апрель 18, 2008 at 07:13 PM MSD #
и на мой взгляд у этих правил есть только одно следствие, касающееся порядка операций, используемое на практике: операции чтения и записи volatile переменных не могут быть переупорядочены с операциями чтения и записи других volatile и не-volatile переменных
Читал Java Lang Specification как раз с целью обоснованно сделать такой вывод (то же самое и про семантику synchronized), но не вышло. Не нашёл я там нужных ограничений на реализацию JVM. Мот стоило бы чуть подробнее остановиться на этом? Это ведь ключевой момент JMM, ради которого, во многом, она и вводилась.
опубликовал Дмитрий Апрель 23, 2008 at 09:39 AM MSD #
Дмитрий,
Вот цитаты из JLS3, которые объясняют, то что операции с volatile-переменными не могут быть переупорядочены c операциями с другими переменными:
http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.4
17.4.4 Synchronization Order:
"A write to a volatile variable (§8.3.1.4) v synchronizes-with all subsequent reads of v by any thread (where subsequent is defined according to the synchronization order)."
17.4.5 Happens-before Order
"If x and y are actions of the same thread and x comes before y in program order, then hb(x, y)."
"If an action x synchronizes-with a following action y, then we also have hb(x, y)."
И завершающий аккорд:
"It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal."
опубликовал Кирилл Широков Май 01, 2008 at 06:05 AM MSD #
Спасибо. Да, выводы можно сделать, но мне они кажутся не стопроцентно строгими - в спецификации есть пара мест, где нет ческих формулировок - остаётся только домысливать.
В частности, могут ли действия x, расположенные ЗА критуческой секцией s, т.е. hb(s, x) быть видимы для критической секции относительно того же монитора в другом потоке (это касательно переупорядочивания инструкций).
Или: при входе в критическую секцию есть 2 версии переменной - измененная глобальная переменная и изменённая локальная - какая из них будет браться конкретной реализацией? Это ситуация "мерж-конфликта". Ответа в спецификации я не вижу, и, возможно, допустима любая реализация. Но это тот момент, который хотелось бы увидеть явно. Согласитесь, достаточно важный случай (хотя такой код, скорее всего, некорректно синхронизирован).
опубликовал Дмитрий Май 12, 2008 at 09:46 AM MSD #