Вопрос с собеседования: что такое Java Memory Model и happens-before
Published: 2024-01-16 12:51:39

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**) в рамках одного потока происходят последовательно. 
Например, у нас такая программа:

```java
int i = 10;
i += 10;
j = i;
a = b + c;
```
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.