DEV Community

faangmaster
faangmaster

Posted on

Вопрос с собеседования: что такое Java Memory Model и happens-before

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

Java Memory Model определена в терминах actions(действий). Actions включают в себя чтение и запись в переменные, lock и unlock мониторов (получение и отпускание мониторов/локов), запуск (start) и join потоков. JMM задает частичный порядок, который называется happens-before, для всех действий (actions) в рамках программы. Для того, чтобы гарантировать, что поток, который выполняет действие B может видеть результат действия A (в не зависимости A и B выполняются в одном потоке или нет), необходимо, чтобы существовала happens-before связь между A и B. Если такой связи нет, то JVM может выполнять действия A и B в произвольном порядке. И никаких гарантий видимости не накладывается.
Отношение happens-before является отношением частичного порядка между двумя операциями. Если одна операция происходит-до другой, то ее результат видим и упорядочен для другой.

Почему happens-before — это частичный порядок?

Потому что оно удовлетворяет определению частичного порядка.

  • Рефлексивность. Результат самой операции, очевидно, видим и упорядочен для нее самой.

  • Антисимметричность. Если результат операции A видим и упорядочен для операции B, то результат операции B бы не видим и не упорядочен для A.

Если бы B была бы видна для A, это значит, что для ее (A) локального времени B произошла до A. Так как результат A виден для B, то это значит, что для ее (B) локального времени A произошла до B. Так как отношение "произошел до/был раньше" транзитивно, из этого следует что B произшла до B. Получаем противоречие. Следовательно, B не может быть видна для A.

  • Транзитивность. Если результат операции A видим и упорядочен для B и результат B видим и упорядочен для C, то результат A видим и упорядочен для С.

Правила happens-before:

Program order rule. В рамках одного потока, любое действие происходит до (happens-before) каждого действия в этом же потоке, которое идет далее в порядке кода программы. Т.е. можно считать, что действия (не любой код, а именно actions) в рамках одного потока происходят последовательно.
Например, у нас такая программа:

int i = 10;
i += 10;
j = i;
a = b + c;
Enter fullscreen mode Exit fullscreen mode

JVM для оптимизации может изменять порядок выполнения кода в программе(reordering). Но JMM накладывает ограничения, что строки i += 10; j = i; не могут быть выполнены параллельно. Т.к. в i += 10; выполняется запись в переменную i, а в строке j = i; происходит чтение. Обе эти строки являются действиями (actions) и на них расспространяются правила happens-before задаваемыми JMM. А строки j = i; a = b + c; могут быть выполнены в произвольном порядке, в том числе параллельно или в обратно порядке. Сначала a = b + c; а потом j = i;

Monitor lock rule. Unlock на мониторе происходит до (happens-before) последующего получения лока на том же мониторе. Т.е. нельзя получить лок до того, как его отпустили. Если кто-то начал получать лок на мониторе, все остальные не смогут его получить, до того, как поток, который получает лок, его не отпустит.

Volatile variable rule. Запись в volatile поле happens-before любого последующего чтения из того же самого поля. То же самое применимо и для atomic переменных.
Т.е. если у нас началась запись в volatile поле, то все, кто будут читать эту переменную, после начала записи, увидят корректно записанное значение, а не в каком-то промежуточном состоянии или старое значение, до начала записи (visibility). Достигается это специальной работой с кэшами процессора и инстукциями компилятору и JVM. Например, запись в volatile переменную будет записано (flushed) в основную память, а не в кэш процессора или его регистры. Также, все кто будет читать volatile поле будет читать из основной памяти (RAM), а не из кэша. Также инструкции компилятору и JVM запрещают изменять порядок для операций с volatile полями.

Thread start rule. Вызов Thread.start на потоке happens-before любого действия в запущенном потоке. Никакое действие из потока не может выполниться до того как был вызван start на этом потоке.

Thread termination rule. Любое действие в потоке происходит до того (happens-before), как любой другой поток детектирует, что поток был прерван, или посредством возвращения из Thread.join или Thread.isAlive возвращает false. Т.е. если Thread.isAlive вернул false, это гарантирует, что в этом потоке уже никакие действия выполнятся не будут. Или мы ждали завершения потока, путем вызова t2.join(), и поток, который ждал завершения t2, возобновился, это гарантирует, что в t2 уже не будут выполняться никакие действия.

Interruption rule. Вызов interrupt на потоке произойдет до того (happens-before) как прерванный поток детектирует прерывание (при помощи InterruptedException или вызова методов isInterrupted или interrupted). Т.е. если один поток вызовет interrupt, то если второй после этого прочитает isInterupted, то он получит корректное обновленное значение. Не в промежуточном состоянии и не с старом состоянии до прерывания. Также не может быть наоборот, что еще не вызвали interrupt, а уже isIntrrupted() возвращает true.

Finalizer rule. Конструктор выполнится до конца до (happens-before) начала выполнения finalizer для того же объекта. Т.е. если у нас начал создаваться объект (был вызван конструктор объекта), то он выполнится до конца до того, как начнет выполнятся finalizer (например, перед тем как GC посчитает его мусором и будет удалять).

Transitivity. Если A happens-before B и B happens-before C, то A happens-before C.

Top comments (0)