JMM

JMM устанавливает правила использования потоками разделяемой памяти

Модели памяти процессоров

Сильная модель

Все видят одинаковые значения.

Слабая модель

Предпологает наличие барьеров памяти.

  1. Flush - Инструкция на сброс данных (отчистка кэша (сброс) в основную память).

  2. Инвалидация - объявление данных в кэше недействительными (значения впредь берутся из основной памяти).

Критическая секция

Критическая секция - участок кода, где используется доступ к общему ресурсу.

Не должно в критической секции быть более одного потока в один момент времени (~ или же могут быть несколько читателей).

Решает проблемы Race Condition.

Cache coherence protocol

Cache coherence - Это (применительно к многопроцессорным системам с кэш-памятью) свойство согласованности данных, вообще говоря разбросанных по различным кэшам. Очевидно, что если у нас одни и те же данные могут быть скэшированы в кэш-памяти разных уровней, и в локальных блоках кэш-памяти разных процессоров/ядер - при модификации этих данных легко может случиться, что существует множество несогласованных версий одних и тех же данных.

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

Подслушивание (snooping)

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

The protocol CPUs use to transfer cache lines between main memory and other CPUs.

MSI

MSI - базовый Cache coherence protocol - работает в многопроцессорных системах. Как и в других протоколах когерентности кэша, абревиатура протокола идентифицируют возможные состояния, в которых может быть строка (cache line):

  • M (Modified) - строка была изменена в кеше. Данные в кэш-памяти теперь несовместимы с хранилищем (например, памятью). Кэш со строкой в состоянии «M» отвечает за запись строки в хранилище, когда она убирается из кэша (evicted), т.е. происходит flush.

  • S (Shared) - Эта строка не изменена и существует в состоянии только для чтения по крайней мере в одном кэше. Кэш может удалить данные, не записывая их в хранилище.

  • I (Invalid) Эта строка либо отсутствует в текущем кэше, либо была аннулирована запросом шины (напр. строка по этому же адресу перешла в состояние Modified. Инвалидация), и должена быть извлечена из памяти или другого кэша, если строка должна храниться в этом кэше.

Fasle Sharing

Fasle Sharing - ситуация, при которой системы с реализованными протоколами когерентности кешей, ухудшают свою производительность, из-за того что не связанные переменные входят в один и тот же cache-line.

Note

cache-line имеет разный размер на разных процессорах – от 32 до 128 байт.

Правила конструирования объектов

Если объект правильно построен (без утечек ссылок в конструкторе объекта и без исключений) - все потоки будут видеть корректное состояние его final полей

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

    Но если после проставления в final поле ссылки - объект по этой ссылке будет изменен - нет гарантий на корректное отображение. Он может быть изменен даже в том же конструкторе:

    data = new int[]{1,2}; data[1] = 10;
    // после правильного конструирования data != null,
    // НО (в других потоках) data[1] может быть == 2
    
  • Любое поле (в том числе final) может быть некорректно отображаемое, при публикации ссылки (досрочная публикация) на this в конструкторе (в т.ч. запись его в static поля), даже в текущем потоке, из-за перестановки.

    Только после завершения конструктора можно гарантировать видимость проставленного в final поле значения Любой анонимный объект созднный в конструкторе имеет неявную ссылку на текущий, оборачивающий объект. Так же при запуске потока из конструктора присутствует досрочная публикация.

  • Сначала происходит построение объекта - потом присваивание ссылки на него.

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

volatile

volatile - используется для данных, которые могут храниться только в heap (~~~)

  • Обращение не в кэш процессора за значением, а напрямую в память (visibility).

    Кэш хранит копию часто используемых данных из RAM, обычно не больших блоков. Могут быть как из heap, так и из стека.

    cashline - то что считывается в кэш при доступе к определенному участку памяти.

  • Атомарные операции чтения и записи для long и double (на 32 разрядных процессорах).

  • Переупорядочивание volatile переменных не происходит.

    Volatile переменные являются memory барьерами, в отношении любой операции чтения, произошедшей после чтения volaile, и любой операции записи, до записи в volaile переменную.

a = 1; volatile = 2; read(volatile); read(a)

Гарантируется Program Order:
\(setA[X] \Rightarrow setVolatile[X] \Rightarrow showVolatile[X] \Rightarrow showA[X]\)

Out-Of-Order Excecution (Внеочередное исполнение) машинных инструкций — исполнение машинных инструкций не в порядке следования в машинном коде (как было при выполнении инструкций по порядку (англ. in-order execution)), а в порядке готовности к выполнению. Реализуется с целью повышения производительности вычислительных устройств

Правила межпоточного взаимодействия. Happens-before

  • Отношение Happens-before (->):
    \(Operation1[Thread1] \rightarrow Operation2[Thread2]\)

    Означает, что все изменения, выполненные Потоком1 до момента Операции1, а также изменения, которые повлекла эта операция - будут видны Потоку2 на Операции2 и после. Happens-before обеспечивает Flush кэша Потока1 по достижению Операции2

  • Happens-before обладает свойством транзитивности:
    \(A[X] \rightarrow B[Y] \rightarrow C[Z] \sim A[X] \rightarrow C[Z]\)

  • Отношение Program Order (=>):
    \(A[X] \rightarrow B[X] \sim A[X] \Rightarrow B[X]\)

    Описывает порядок Program Order, который определяет отношение Happens-before только на явных зависимых элементах, которые нальзя переставить друг с другом:

  • Race Condition:
    \(SetVal[X] \rightarrow \begin{Bmatrix} SetVal[Y] \Rightarrow ShowVal[Y] \\ SetVal[Z] \Rightarrow ShowVal[Z] \end{Bmatrix}\)


Освобождение монитора

(Освобождение монитора) hb (каждого последующего заполучения того же самого монитора)

\[\begin{split}(EnterMonitor[X] \Rightarrow SetVal(myVal)[X] \Rightarrow ExitMonitor[X]) \rightarrow ... \\ ... \rightarrow (EnterMonitor[Y] \Rightarrow ShowVal[Y] \Rightarrow ExitMonitor[Y])\end{split}\]

Выведет myVal, при том, что поле может быть не volatile!, так как на каждом hb выполняется flush (~~~)


\[\begin{split}SetVal(myVal)[X] \Rightarrow (EnterMonitor[X] \Rightarrow ExitMonitor[X]) \rightarrow ... \\ ... \rightarrow (EnterMonitor[Y] \Rightarrow ExitMonitor[Y]) \Rightarrow ShowVal[Y]\end{split}\]

Выведет myVal

Все что случилось до hb и после (до выхода из монитора и после входа) - может восприниматься, как операции, происходящие в одном потоке с точки зрения JMM

\[\begin{split}lock.unlock[X] \rightarrow \begin{bmatrix} lock.lock[Y] \\ if \, lock.tryLock[Y] \end{bmatrix}\end{split}\]

То же самое, что и с захватом монитора

Операция записи в volatile-поле

(Операция записи в volatile-поле) hb (каждой последующей операции чтения того же самого поля)

\[\begin{split}SetVolatile1(1)[X] \Rightarrow SetVolatile2(2)[X] \rightarrow ... \\ ... \rightarrow when \, IsVolatile2(== 2)[Y] \Rightarrow ShowVolatile1[Y]\end{split}\]

выведет 1

Program Order (Happens-before) между записью в volatile поля гарантируется из-за модификатора volatile, а именно из-за того, что Volatile2 является таковым, т.е. memory барьером

\[SetVal(1)[X] \Rightarrow SetVolatile2(2)[X] \rightarrow when \, IsVolatile2(== 2)[Y] \Rightarrow ShowVal[Y]\]

выведет 1

Состояния потоков

\[\begin{split}X.run[X] \rightarrow \begin{bmatrix} X.join[Y] \\ if \, (X.isAlive == false)[Y] \end{bmatrix}\end{split}\]


\[\begin{split}X.interrupt[Y] \rightarrow \begin{bmatrix} catch \, throwInterruptedException[X] \\ if \, X.isInterrupted[Z] \\ if \, interrupted[X]) \end{bmatrix}\end{split}\]


\[SetValue[Y] \Rightarrow X.interrupt[Y] \rightarrow if \, interrupted[X] \Rightarrow ShowValue[X]\]

как и в примерах выше с отношениями hb

Конструирование объекта

\[CorrectConstructed[X] \rightarrow finalize[Y]\]


\[\begin{split}StartConstructor[X] \Rightarrow SetVal[X] \Rightarrow CorrectConstructed[X] \rightarrow ... \\ ... \rightarrow finalize[Y] \Rightarrow ShowVal[Y]\end{split}\]

CAS

Обычный CAS имеет семантику volatile read+write. В частности, это означает, что между двумя CAS-ами одной переменной всегда существует ребро happens-before