Правила «происходит до» в модели памяти Java

В настоящее время я готовлюсь к экзамену по параллельному программированию и не понимаю, почему результат этой программы равен 43. Почему x = y + 1 выполняется раньше t.start()? Я также должен объяснить, что происходит до правил, которые я использовал.

Если я понимаю правило порядка выполнения программы (каждое действие в потоке происходит перед каждым действием в этом потоке, которое следует позже в порядке выполнения программы), t.start() должно быть выполнено до x = y + 1, чтобы поток t скопировал переменную x, которая будет равна 1.

public class HappensBefore {

static int x = 0;
static int y = 42;

public static void main(String[] args) {
    x = 1;
    Thread t = new Thread() {
        public void run() {
            y = x;
            System.out.println(y);
        };
    };
    t.start();
    x = y + 1;
}

person lmaonuts    schedule 27.01.2018    source источник


Ответы (5)


Согласно JMM:

Вызов start() в потоке происходит до любых действий в запущенном потоке.

а также

Если x и y являются действиями одного и того же потока и x предшествует y в программном порядке, то hb(x, y).

Определение порядка программы таково:

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

Семантика между потоками — это четко определенная концепция в JMM. Это означает, что порядок инструкций в программе, выполняемой каждым потоком, должен сохраняться таким, как он записан в тексте программы.

Применяя все это к вашему делу:

t.start(); hb x = y + 1; //порядок программы

t.start(); hb y = x; // задано правило "происходит до" здесь

Без дополнительной синхронизации мы не можем сказать, как x = y + 1; и y = x; соотносятся друг с другом (с точки зрения JMM).

Если вы пытаетесь ответить на вопрос «Что может произойти во время выполнения в моем случае?». Может случиться много вещей... Взгляните на этот ответ. Среда выполнения может выполнять нетривиальные оптимизации, совместимые с JMM.

В любом случае, если вас интересует внутреннее устройство, вы можете взглянуть на эту статью (барьеров памяти можно избежать). Как видно из сгенерированной сборки, при чтении volatile не применяется барьер памяти. Я имею в виду, что время выполнения можно оптимизировать в любом случае... Пока сохраняются правила JMM...

person St.Antario    schedule 27.01.2018
comment
Изменится ли что-то, если переменная x будет изменчивой? Можем ли мы тогда сказать, что на выходе всегда будет 43? - person lmaonuts; 27.01.2018
comment
@lmaonuts Нет, это ничего не изменит. Вы все еще не знаете, какой из x = y + 1 и y = x будет выполнен первым. - person Erwin Bolwidt; 27.01.2018
comment
@lmaonuts Нет. Давайте попробуем применить JMM к этому случаю. volatile-write хб subsequent volatile-read. Вы можете только гарантировать, что если изменчивое чтение в y = x; произошло после изменчивой записи x = y + 1; во время выполнения, тогда вы наблюдаете фактическое значение (чтение и запись целых чисел являются атомарными, в случае длинных могут быть проблемы. .. это не атомарно). Но x = y + 1; и y = x до сих пор не бывает. - person St.Antario; 27.01.2018
comment
@lmaonuts Добавлена ​​статья, которую может быть интересно прочитать. - person St.Antario; 27.01.2018

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

Да, t.start() будет выполняться до x = y + 1. Но запуск потока не означает, что тело потока выполняется до x = y + 1. Он мог работать до, после или чередоваться с остальной частью main().

person John Kugelman    schedule 27.01.2018
comment
поэтому возможные значения y могут быть 0, 1 или 43? У меня также есть 2 дополнительных вопроса: 1) Какое значение будет присвоено потоку t для y? Объясните, что происходит до правил, которые вы использовали. 2) Какое значение (возможные значения) будет присвоено основному потоку y после запуска потока t? Объясните, что происходит до правил, которые вы использовали. Что, если переменная x была изменчивой? - person lmaonuts; 27.01.2018
comment
@lmaonuts Я думаю, что y не может быть 0, потому что у нас есть hb(t.start(), y = x). Вы поймете happens-before, если поймете это. См. пример здесь stackoverflow.com/a/48489797/3405171. - person v.ladynev; 30.01.2018

Если я понимаю правило порядка выполнения программы (каждое действие в потоке происходит перед каждым действием в этом потоке, которое происходит позже в порядке выполнения программы)

Нет.
Здесь у вас не один поток, а два потока: основной поток и поток, созданный и запущенный основным потоком:

Thread t = new Thread() {...};
t.start();

А живые потоки JVM по своей природе параллельны.

Если бы два оператора были выполнены в одном и том же потоке, можно было бы с уверенностью предположить, что выведенное значение будет равно «1». Но если два оператора выполняются в отдельных потоках, выводимое значение вполне может быть «0», потому что нет гарантии, что изменение потока A на счетчик будет видно потоку B, если только программист не установил отношение «происходит до» между эти два утверждения.

Отношение "происходит до" возникает только в том случае, если все операторы выполняются одним и тем же потоком или если вы явно создаете отношение "происходит до".

Выдержка из учебника по ошибкам непротиворечивости памяти:

Есть несколько действий, которые создают отношения «случается раньше». Одним из них является синхронизация, как мы увидим в следующих разделах.

Как сказано выше, операторы выполняются двумя потоками.
И вы не синхронизируете операторы явно.
Таким образом, между потоками возникает состояние гонки, и, как правило, порядок выполнения следует рассматривать как непредсказуемый. . Теперь на практике для такого короткого оператора: x = y + 1 :

t.start(); 
x = y + 1;

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

Кроме того, современные ЦП имеют несколько ядер.
Таким образом, если у ЦП есть доступные потоки, основной поток не будет приостановлен, чтобы запустить новый поток.
Таким образом, два потока будут выполняться "в одно и то же время".
Но поскольку x = y + 1; выполняется гораздо быстрее при запуске и запуске потока, первый оператор может завершиться только раньше второго.

person davidxxx    schedule 27.01.2018

t.start предшествует x = y + 1, это не гарантирует, что каждая строка кода в методе run() будет выполнена до x = y + 1.

На самом деле распечатанный результат без синхронизации неопределенен из-за состояния гонки, это может быть 1 или 43.

person xingbin    schedule 27.01.2018
comment
Когда будет выход 2? - person lmaonuts; 27.01.2018
comment
@lmaonuts Моя ошибка, это может быть 1 или 43. Это не может быть 0, потому что t.start() происходит перед методом run. - person xingbin; 27.01.2018

Я хотел бы добавить, что код в основном методе выполняется в основном потоке, а Thread t не блокирует выполнение Main в вашем примере. Вот почему строка x = y + 1 может выполняться быстрее, чем тело потока t (как уже указал @davidxxx).

Вы можете наблюдать другое поведение, если вы добавите t.join() после t.start():

...
t.start();
t.join();

В этой ситуации основной поток будет ждать завершения потока t, и на выходе будет 1.

person Oleksandr Pyrohov    schedule 03.02.2018