Что такое happens before java
Перейти к содержимому

Что такое happens before java

  • автор:

Happenes before

“Happens-before” — это понятие из Java Memory Model (JMM), которое описывает отношение между операциями чтения и записи в разделяемых переменных. Понятие “happens-before” обеспечивает определенные гарантии относительно порядка выполнения операций в разных потоках

Главное правило “happens-before” ;

Если операция записи (write) в разделяемой переменной произошла “happens-before” операции чтения (read) этой же переменной, то любое значение, записанное в эту переменную до операции записи, должно быть видимым при операции чтения.

Операции, которые создают “happens-before”, включают в себя:

  1. Завершение конструктора объекта: Когда объект инициализируется в одном потоке, и его ссылка становится видимой в другом потоке после завершения конструктора, “happens-before” создается.
  2. Завершение вызова Thread.start() : Когда поток запускается с использованием метода start() , он не начинает выполнение кода до завершения этого метода, что создает «happens-before».
  3. Завершение вызова Thread.join() : Когда поток вызывает метод join() для другого потока, он блокируется до тех пор, пока другой поток не завершит выполнение, что также создает «happens-before».

“Happens-before” является важной концепцией для правильного синхронизированного доступа к разделяемым данным в Java и обеспечивает корректное взаимодействие между потоками.

Java Memory Model

Модель памяти Java (Java Memory Model, JMM) описывает поведение потоков в среде исполнения Java. Модель памяти — часть семантики языка Java, и описывает, на что может и на что не должен рассчитывать программист, разрабатывающий ПО не для конкретной Java-машины, а для Java в целом.

Исходная модель памяти Java (к которой, в частности, относится “потоколокальная память”), разработанная в 1995 году, считается неудачной: многие оптимизации невозможно провести, не потеряв гарантию безопасности кода. В частности, есть несколько вариантов написать многопоточного “одиночку”:

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

Поэтому механизм работы памяти был переработан. В 2005 году, с выходом Java 5 был презентован новый подход, который был еще улучшен с выходом Java 14.

В основе новой модели лежат три правила:

Правило № 1: однопоточные программы исполняются псевдопоследовательно. Это значит: в реальности процессор может выполнять несколько операций за такт, заодно изменив их порядок, однако все зависимости по данным остаются, так что поведение не отличается от последовательного.

Правило № 2: нет невесть откуда взявшихся значений. Чтение любой переменной (кроме не-volatile long и double, для которых это правило может не выполняться) выдаст либо значение по умолчанию (ноль), либо что-то, записанное туда другой командой.

И правило № 3: остальные события выполняются по порядку, если связаны отношением строгого частичного порядка “выполняется прежде” (happens before).

Happens before

Лесли Лэмпорт придумал понятие Happens before. Это отношение строгого частичного порядка, введенное между атомарными командами (++ и — не атомарны) и не означающее “физически прежде”.

Оно говорит о том, что вторая команда будет “в курсе” изменений, проведенных первой.

Happens before

Например, одно выполняется прежде другого для таких операций:

Синхронизация и мониторы:

  • Захват монитора (метод lock , начало synchronized) и все, что происходит в том же потоке после него.
  • Возврат монитора (метод unlock , конец synchronized) и все, что происходит в том же потоке перед ним.
  • Возврат монитора и последующий захват другим потоком.

Запись и чтение:

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

Обслуживание объекта:

  • Статическая инициализация и любые действия с любыми экземплярами объектов.
  • Запись в final-поля в конструкторе и все, что после конструктора. Как исключение – соотношение happens-before не соединяется транзитивно с другими правилами и поэтому может вызвать межпоточную гонку.
  • Любая работа с объектом и finalize() .

Обслуживание потока:

  • Запуск потока и любой код в потоке.
  • Зануление переменных, относящихся к потоку, и любой код в потоке.
  • Код в потоке и join() ; код в потоке и isAlive() == false .
  • interrupt() потока и обнаружение факта остановки.

Нюансы работы Happens before

Освобождение (releasing) монитора happens-before происходит прежде, чем получение (acquiring) того же монитора. Стоит обратить внимание, что именно освобождение, а не выход, то есть за безопасность при использовании wait можно не беспокоиться.

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

 public class Keeper < private Data data = null; public Data getData() < synchronized(this) < if(data == null) < data = new Data(); >> return data; > > 

Запись в volatile переменную happens-before чтение из той же переменной. То изменение, которое мы внесли, конечно, исправляет некорректность, но возвращает того, кто написал изначальный код, туда, откуда он пришел — к блокировке каждый раз. Спасти может ключевое слово volatile. Фактически, рассматриваемое утверждение значит, что при чтении всего, что объявлено volatile, мы всегда будем получать актуальное значение.

Кроме того, как я говорил раньше, для volatile полей запись всегда (в том числе long и double) является атомарной операцией. Еще один важный момент: если у вас есть volatile сущность, имеющая ссылки на другие сущности (например, массив, List или какой-нибудь еще класс), то всегда “свежей” будет только ссылка на саму сущность, но не на все, в нее входящее.

Итак, обратно к нашим Double-locking баранам. С использованием volatile исправить ситуацию можно так:

 public class Keeper < private volatile Data data = null; public Data getData() < if(data == null) < synchronized(this) < if(data == null) < data = new Data(); >> > return data; > > 

Тут у нас по-прежнему есть блокировка, но только в случае, если data == null. Остальные случаи мы отсеиваем, используя volatile read. Корректность обеспечивается тем, что volatile store happens-before volatile read, и все операции, которые происходят в конструкторе, видны тому, кто читает значение поля.

Комментарии (7)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
mega478 Уровень 24
24 октября 2023
. создаст два одиночки; — Русский речь меня покинул!
Олег Уровень 111 Expert
25 августа 2023

Ну кто так излагает:»Запись в volatile переменную happens-before чтение из той же переменной. То изменение, которое мы внесли, конечно, исправляет некорректность, но возвращает того, кто написал изначальный код, туда, откуда он пришел — к блокировке каждый раз. Спасти может ключевое слово volatile. Фактически, рассматриваемое утверждение значит, что при чтении всего, что объявлено volatile, мы всегда будем получать актуальное значение.» Как-будто машинный перевод редактировал первокласник. Можно было хотя бы так изложить: «Использование ключевого слова volatile позволяет обеспечить последовательность операций в многопоточной среде. Когда переменная помечается как volatile, это указывает, что её значения могут изменяться разными потоками. Основной эффект заключается в том, что запись значения в переменную, помеченную как volatile, «происходит до» (happens-before) любого последующего чтения этого значения из другого потока. Таким образом, любое значение, записанное в volatile переменную одним потоком, будет гарантированно видимо другим потокам, которые читают это значение. Это обеспечивает предсказуемость и последовательность операций чтения и записи в среде с несколькими потоками. Однако необходимо отметить, что хотя использование ключевого слова volatile может помочь обеспечить видимость изменений между потоками, оно может быть ограничено в сложных сценариях синхронизации. В некоторых случаях более сложные механизмы синхронизации, такие как блокировки, могут потребоваться для обеспечения корректности и безопасности программы.»

Валера Калиновский Уровень 32
31 июля 2022

хотелось бы побольше примеров из жизни. По сути в конце написан код очень похожий на trade-safe код для создания синглтона. Вот уж никогда не думал что в этом коде использовался принцип happens before. Почему то никто об этом не упоминает когда разбирает этот код. А что было бы еслиб не было этого принципа? Можно привести код в котором чтото ломается из-за того что нет happens before

Другие детали синхронизации и многонитиевости

Есть такая здоровенная тема, называется Java Memory Model. В принципе знать ее тебе пока не обязательно, но услышать про это – будет полезно.

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

— Да, лучше и сложнее. Это как самолет. Летать на самолете лучше, чем идти пешком, но сложнее. Попробую объяснить тебе новую ситуацию очень упрощенно.

Вот что было придумано. В код был добавлен механизм синхронизации локальной памяти нитей, названный «happens before» (дословно «случилось перед»). Был придуман ряд правил/условий, при наступлении которых память синхронизируется – обновляется до актуального состояния.

public int y = 1; public int x = 1; x=2; synchronized(mutex)
synchronized(mutex) < if (y == x) System.out.println("YES"); >

Одно из таких условий – это захват освобожденного мютекса. Если мютекс был освобожден и снова захвачен, то перед захватом обязательно выполнится синхронизация памяти. Нить 2 увидит «самые новые» значения переменных x и y, даже если не объявлять их volatile.

— Как интересно. И много таких условий?

— Достаточно, вот некоторые условия синхронизации памяти:

  • В рамках одной нити любая команда happens-before (читается «случается перед») любой операцией, следующей за ней в исходном коде.
  • Освобождение лока (unlock) happens-before захватом того же лока (lock).
  • Выход из synchronized блока/метода happens-before вход в synchronized блок/метод на том же мониторе.
  • Запись volatile поля happens-before чтение того же самого volatile поля.
  • Завершение метода run экземпляра класса Thread happens-before выход из метода join() или возвращение false методом isAlive() экземпляром той же нити.
  • Вызов метода start() экземпляра класса Thread happens-before начало метода run() экземпляра той же нити.
  • Завершение конструктора happens-before начало метода finalize() этого класса
  • Вызов метода interrupt() на нити happens-before, когда нить обнаружила, что данный метод был вызван, либо путем выбрасывания исключения InterruptedException, либо с помощью методов isInterrupted() или interrupted()

— Т.е. все немного сложнее, чем я думал?

— Да, немного сложнее…

— Спасибо, Риша, буду думать.

— Не заморачивайся сильно на эту тему. Придет время, сам все поймешь. Пока тебе лучше разбираться в основах, чем лезть в дебри внутреннего устройства Java-машины.

— О_о. М-да. Некоторые вещи лучше не знать.

Java Memory Model и happens-before

Добрый день. Есть небольшой вопрос по JMM. Я знаю как работает happens-before, но не могу понять один момент. Вот код:

private static int x = 0; private static volatile boolean flag = false; public static void main(String[] args) throws InterruptedException < new Thread(() ->< x = 10; while (!flag) ; System.out.println(x); >).start(); x = 5; flag = true; > 

Какое значение должно принять X? Если какое-то правило чтобы определить это?
Отслеживать
25.1k 4 4 золотых знака 46 46 серебряных знаков 81 81 бронзовый знак
задан 23 июл 2016 в 23:26
251 2 2 серебряных знака 6 6 бронзовых знаков

Любое из предложенных. Порядок присвоений 5 и 10 не определен, он может быть любым, порожденный тред может начать работу раньше x = 5 и наоборот. Насколько понимаю, happens-before в данном случае распространяется только на flag , поэтому два разных треда могут существовать с разными значениями..

23 июл 2016 в 23:57

Да, я тоже думаю, что может быть либо 5 либо 10. НО если взять теорию JMM то как раз прочтение переменной flag в потоке 2(не main) означает, что транзитивное замыкание и получается порядок становится полным!, то есть: 1) x = 5; 2) flag = true; 3) while (!flag) 4) System.out.println(x); Но вот как к этому порядку относится x = 10; понять не могу, ведь внутри потока тоже порядок полный. Получается 2 полных порядка сливаются как-то?

24 июл 2016 в 0:16
прочтение переменной flag влияет только на видимость переменной flag
24 июл 2016 в 13:44

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Если изобразить потоки выполнения в вашем коде, мы получим такую картину:

введите сюда описание изображения

Main thread — это основной поток вашей программы, Thread1 — поток, который вы явно создаете.

Запись и чтение волатильной переменной flag действительно создают отношение Happens-Before (E → B на диаграмме). Таким образом по отношению к вызову System.out.println(x); у нас есть два потока выполнения, для каждого из которых гарантируется порядок выполнения: A → B → C и D → E → B → C. Запись в переменную x произойдет гарантированно раньше ее чтения.

Но вот инструкции записи в переменную x (A и D) находятся в состоянии гонки (race condition) и их порядок выполнения друг относительно друга не определен. В итоге то, что мы видим на диаграмме никак нельзя назвать полным порядком. JVM вполне может выполнить инструкции следующим образом:

введите сюда описание изображения

Отслеживать
ответ дан 24 июл 2016 в 11:32
34.6k 15 15 золотых знаков 66 66 серебряных знаков 95 95 бронзовых знаков

«Запись в переменную x произойдет гарантированно раньше ее чтения.» данное предложение читается как будто один поток будет приостановлен пока второй не установит новое знание — но это не так.

25 июл 2016 в 18:35

@MikhailovValentine а тут без разницы, какой поток установит значение первым, все равно чтение произойдет после обоих записей

25 июл 2016 в 19:02

@Etki, да все верно, хотел указать на другое выполнение D → E → B → C не обязательно будет иметь место т.к. возможно что сначала произойдет D → E а потом A → B → C и в таком случае диаграмма будет иметь другой вид.

26 июл 2016 в 3:52

@MikhailovValentine да все так же. Стрелка между двумя тредами просто показывает, что операция B обязана произойти после операции А. Порядок остальных операций между тредами не определен, эту диаграмма вполне читается как D-E-A-B-C.

26 июл 2016 в 20:39
@Etki, спасибо за апологетику. все именно так
26 июл 2016 в 20:45

Мы не знаем какое значение увидит инструкция System.out.println(x); по нескольким причинам:

  • Поток может прочитать значение, установленное в нем (независимо было ли оно установлено раньше или позже другого потока) или значение, установленное в другом потоке. В виду того что гарантии на то, какое значение прочитает поток не даются.
  • В виду того, что невозможно определить время выполнения инструкции в различных поток относительно других потоков — неизвестно в какой момент потоки выполнят инструкции x = 10 и flag = true относительно друг друга.

Относительно happens-before происходящего с volatile переменной. В данном случае если чтение volatile переменной происходит после записи ее переменной, то операция чтения ОБЯЗАТЕЛЬНО будет видеть «новые» данные. В данном случае нам это говорит лишь о том, что поток который находится в цикле выйдет из него, как только будет выполнена инструкция flag = true . Но опять же о том, когда будет выполнена инструкция flag = true нам неизвестно, она может быть выполнена как до, так и после чтения (одного или нескольких) значения из volatile переменной в порожденном потоке.

Необходимо отметить, что отношение, изображенное на картинке в ответе Nofate не должно трактоваться, как обязательный порядок индукцией или блокировка читателя до момента пока не будет установлено значение volatile переменной.

“Запись в переменную x произойдет гарантированно раньше ее чтения.”

Поток просто будет находится в бесконечном цикле до момента выполнения flag = true .

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

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