Java Memory Model.

Модель памяти Java — это набор гарантий, которые получает программист при написании многопоточного кода. Поведение вашей программы становится для вас более предсказуемым. Ознакомившись с моделью памяти Java вы будете лучше понимать работу вашего кода в многопоточной среде и перестанете писать такой код, который заставляет вас каждый раз при выполнении программы удивленно вскинуть брови вверх.  Добро Пожаловать под кат 🙂

Чтобы писать правильный и эффективный многопоточный код, нужно приложить немало усилий. Основная проблема многопоточного кода — разделяемые ресурсы. Говоря о разделяемых ресурсах,  я в первую очередь подразумеваю память. Что произойдет, если два потока одновременно получат доступ к одной и той же переменной? А если один поток получит ссылку на объект, который был еще не до конца инициализирован? Какое именно значение они увидят в итоге?

Есть и другие проблемы. Потоки могут записывать значения переменных в свой локальный кэш, не сбрасывая результаты в main memory, а компилятор(о ужас!) менять порядок выполнения инструкций в вашем коде для повышения быстродействия. А теперь представьте, что всего этого вы не знаете? Значит, вы можете написать код, который будет вести себя совершенно непредсказуемым образом!

Именно для этого в Java 5 была пересмотрена старая модель памяти Java, первая версия которой появилась еще в далеком 1995 году. Модель памяти — это те гарантии в работе многопоточного Java кода, зная которые вы сможете уберечь себя от совершенно непредсказуемого поведения программы.

Атомарность.

Не все действия над примитивными типами атомарны. Для типов long и double операции записи не являются атомарными. То есть сначала записываются старшие разряды, а затем младшие. Пусть у нас есть следующий класс:

Объект класс Data будет просто хранить переменную типа long и имеет два метода для получения значения этой переменной и его изменения. А теперь допустим, что к этому объекту обращаются из разных потоков:

 

 

Первый поток читает значение переменной, а второй его записывает. Вот как число 15 будет выглядеть в двоичном представлении:

А теперь представим ситуацию: второй поток записал старшие биты и тут планировщик потоков отдает управление первому потоку. В итоге первый поток получит значение 0, то есть увидит значение переменной в промежуточном состоянии. Значит доступ к подобным переменным нужно синхронизировать.

Видимость.

У каждого потока в Java есть свой локальный кэш. И данные из кэша в основную память сбрасываются несразу. В примере выше также могла возникнуть ситуация, при которой поток 2 правильно записал бы значение переменной а, но поток 1 все равно бы его не увидел, прочитав значение этой переменной из кэша. Для того, чтобы исключить непредсказуемость, следует пометить эту переменную ключевым словом volatile. Эта декларация обязывает потоки всегда синхронизировать значения таких переменных с основной памятью.

На самом деле ключевое слово volatile дает нам еще одну гарантию,  о которой мы поговорим чуть ниже.

Reordering.

Может быть вы не знали, но при выполнении вашей программы компилятор и даже процессор могут менять порядок выполнения операторов в вашей программе! То есть код может быть выполнен в несколько другом порядке, если компилятор или процессор посчитают, что это будет более эффективно. Но конечно абсолютно любые инструкции в хаотичном порядке поменяться не могут и модель памяти Java как раз и налагает на reordering свое правило, которому компилятор должен подчиниться и называется оно happens-before.

Давайте разберемся с тем, что это за правило. У нас по-прежнему есть два потока: Thread 1 и Thread 2. Операция А выполняется в потоке Thread 1, а операция B — в потоке Thread 2.

Правило happens-before говорит о том, что если операция А happens-before(выполнилась до) операции B, то все изменения, сделанные самой операцией А и до ее выполнения, будут видны в момент выполнения операции B.

Такое отношение выполняется для процедур освобождения и захвата монитора(получения блокировки на объект). Операция releasing(освобождение) монитора happens-before lock(захват) монитора. На картинке видно, что запись в переменную x происходит до освобождения монитора. А это значит, что второй поток увидит значение x после блокировки. В переменную i будет записана единица. Но для действий, которые находятся между unlock и lock никаких гарантий нет и они могут быть переупорядочены.

Другими операциями, для которых выполняется отношение happens-before, является volatile write и volatile read. То есть volatile write hb volatile read, а значит чтение из volatile переменной произойдет только после окончательной записи в volatile-поле. В примере выше, с классом Data, мы могли пометить нашу переменную a как volatile и избежать тем самым проблем с синхронизацией.

Заключение.

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *