Интерпретация "правила порядка программы" в Java concurrency

Правило правила программы: "Каждое действие в потоке происходит - перед каждым действием в этом потоке, которое приходит позже в порядке выполнения программы"

1.I прочитал в другой поток, что действие

  • читает и записывает в переменные
  • блокировки и разблокировки мониторов
  • начало и соединение с потоками

Означает ли это, что чтение и запись могут быть изменены по порядку, но чтение и запись не могут изменить порядок с действиями, указанными во 2-й или 3-й строках?

2. Что означает "порядок программы"?

Объяснение с примерами будет действительно полезно.

Дополнительный связанный вопрос

Предположим, что у меня есть следующий код:

long tick = System.nanoTime(); //Line1: Note the time
//Block1: some code whose time I wish to measure goes here
long tock = System.nanoTime(); //Line2: Note the time

Во-первых, это однопоточное приложение, которое упрощает задачу. Компилятор замечает, что ему нужно дважды проверять время, а также замечает блок кода, который не имеет зависимости от окружающих строк с указанием времени, поэтому он видит потенциал для реорганизации кода, что может привести к тому, что Block1 не будет окружен вызовами времени во время фактического выполнения (например, рассмотрите этот порядок Line1- > Line2- > Block1). Но я, как программист, вижу зависимость между Line1,2 и Block1. Строка 1 должна немедленно предшествовать Блоку 1, Блок 1 принимает конечное количество времени для завершения и сразу же преуспевает в Line2.

Итак, мой вопрос: правильно ли я измеряю блок?

  • Если да, то что мешает компилятору переупорядочить порядок.
  • Если нет, (что считается правильным после прохождения ответа Enno), что я могу сделать, чтобы предотвратить его.

P.S.: Я украл этот код из другого вопроса, который я недавно спросил в SO.

Ответ 1

Вероятно, это помогает объяснить, почему такое правило существует в первую очередь.

Java - это процедурный язык. То есть вы говорите Java, как сделать что-то для вас. Если Java выполняет ваши инструкции не в том порядке, который вы написали, это, очевидно, не сработает. Например. в приведенном ниже примере, если Java будет делать 2 → 1 → 3, то тушеное мясо будет разрушено.

1. Take lid off
2. Pour salt in
3. Cook for 3 hours

Итак, почему правило не просто говорит: "Java выполняет то, что вы написали в том порядке, который вы написали"? В двух словах, потому что Java умный. Возьмем следующий пример:

1. Take eggs out of the freezer
2. Take lid off
3. Take milk out of the freezer
4. Pour egg and milk in
5. Cook for 3 hours

Если Java похож на меня, он просто выполнит его по порядку. Однако Java достаточно умен, чтобы понять, что он более эффективен и что конечный результат будет таким же, если он сделает 1 → 3 → 2 → 4 → 5 (вам не нужно снова ходить к морозильной камере и который не меняет рецепт).

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

Пока все хорошо. Почему он не делает то же самое в нескольких потоках? В многопоточном программировании Java недостаточно умен, чтобы сделать это автоматически. Это будет для некоторых операций (например, объединение потоков, начальных потоков, когда используется блокировка (монитор) и т.д.), Но для других вещей вам нужно явно указать, чтобы не выполнять переупорядочение, которое изменило бы выход программы (например, маркер volatile на полях, использование замков и т.д.).

Примечание:
Быстрое добавление о "бывает-до отношений". Это причудливый способ сказать независимо от того, что может сделать переупорядочение Java, а материал A будет происходить перед материалом B. В нашем странном более позднем примере тушения "Шаг 1 и 3 происходит до шага 4" Налейте яйцо и молоко в ". Также, например," Шаг 1 и 3 не нуждаются в контакте между событиями, потому что они никак не зависят друг от друга"

О дополнительном вопросе и ответе на комментарий

Сначала давайте определим, что означает "время" в мире программирования. В программировании мы имеем понятие "абсолютное время" (какое сейчас время в мире?) И понятие "относительное время" (сколько времени прошло с x?). В идеальном мире время - это время, но если у нас нет атомных часов, абсолютное время нужно будет корректировать время от времени. С другой стороны, для относительного времени мы не хотим поправок, поскольку нас интересуют только различия между событиями.

В Java System.currentTime() имеет дело с абсолютным временем, а System.nanoTime() - относительным временем. Вот почему Javadoc nanoTime утверждает: "Этот метод можно использовать только для измерения прошедшего времени и не имеет отношения ни к какому-либо другому понятию системного или настенного времени".

На практике, как currentTimeMillis, так и nanoTime являются нативными вызовами, и поэтому компилятор не может практически доказать, что переупорядочение не повлияет на правильность, а это значит, что он не изменит порядок выполнения.

Но представим себе, что мы хотим написать реализацию компилятора, которая на самом деле смотрит на собственный код и переупорядочивает все до тех пор, пока оно законно. Когда мы смотрим на JLS, все, что он говорит нам, это то, что "вы можете переупорядочить все, пока оно не может быть обнаружено". Теперь, как автор компилятора, мы должны решить, нарушит ли переупорядочение семантику. Для относительного времени (nanoTime) это явно бесполезно (т.е. Нарушает семантику), если мы изменим порядок выполнения. Теперь, нарушила бы он семантику, если бы мы переупорядочивали в течение абсолютного времени (currentTimeMillis)? Пока мы можем ограничить разницу с источником мирового времени (пусть говорят системные часы) на все, что мы решаем (например, "50 мс" ) *, я говорю "нет". Для приведенного ниже примера:

long tick = System.currentTimeMillis();
result = compute();
long tock = System.currentTimeMillis();
print(result + ":" + tick - tock);

Если компилятор может доказать, что compute() принимает меньше, чем любое максимальное расхождение с системными часами, которое мы можем разрешить, тогда было бы законно изменить порядок его следующим образом:

long tick = System.currentTimeMillis();
long tock = System.currentTimeMillis();
result = compute();
print(result + ":" + tick - tock);

Так как это не будет нарушать специфицированную нами спецификацию и, следовательно, не будет нарушать семантику.

Вы также спросили, почему это не включено в JLS. Я думаю, что ответ будет "держать JLS коротким". Но я мало знаю об этом мире, поэтому вы можете задать для этого отдельный вопрос.

*: В реальных реализациях эта разница зависит от платформы.

Ответ 2

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

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

x = 1;
z = z + 1;
y = 1;

Компилятор остается свободным для изменения порядка операций, однако он лучше всего подходит для повышения производительности. Один из способов подумать об этом: если вы можете изменить порядок этих операций в исходном коде и получить одинаковые результаты, компилятор может сделать то же самое. (И на самом деле, он может пойти еще дальше и полностью отменить операции, которые, как показано, не имеют никаких результатов, таких как вызовы пустых методов.)

С вашей второй маркерной точкой вступает в действие правило блокировки монитора: "Разблокировка на мониторе происходит до каждой последующей блокировки на этой блокировке главного монитора". (Java Concurrency in Practice стр. 341) Это означает, что поток, получающий данный замок, будет иметь согласованное представление о действиях, которые произошли в других потоках, прежде чем освободить эту блокировку. Однако обратите внимание, что эта гарантия применяется только тогда, когда два разных потока release или acquire имеют ту же блокировку. Если Thread A создает кучу вещей, прежде чем отпускать Lock X, а затем Thread B получает Lock Y, Thread B не гарантирует последовательного представления действий A до X.

Можно читать и записывать переменные, которые должны быть переупорядочены с помощью start и join, если a.), что делает это, не нарушает порядок выполнения внутри потока, а b.) переменные не имеют других "прежде чем" семантика синхронизации потоков" применяется к ним, например, сохраняя их в полях volatile.

Простой пример:

class ThreadStarter {
   Object a = null;
   Object b = null;
   Thread thread;

   ThreadStarter(Thread threadToStart) {
      this.thread = threadToStart;
   }

   public void aMethod() {
      a = new BeforeStartObject();
      b = new BeforeStartObject();
      thread.start();
      a = new AfterStartObject();
      b = new AfterStartObject();

      a.doSomeStuff();
      b.doSomeStuff();
   }
}

Так как поля a и b и метод aMethod() никак не синхронизированы, а действие start thread не изменяет результаты записи в поля (или выполнение материал с этими полями), компилятор может свободно переупорядочить thread.start() в любом месте метода. Единственное, что он не мог сделать с порядком aMethod(), - это перемещение порядка записи одного из BeforeStartObject в поле после записи AfterStartObject в это поле или для перемещения одного из doSomeStuff() на него записываются вызовы на поле до AfterStartObject. (То есть, если предположить, что такое переупорядочение каким-либо образом изменит результаты вызова doSomeStuff().)

Здесь важно иметь в виду, что при отсутствии синхронизации поток, начатый в aMethod(), теоретически может наблюдать одно или оба поля a и b в любом из состояний, которые они взять во время выполнения aMethod() (включая null).

Дополнительный вопрос

Назначения tick и tock не могут быть переупорядочены относительно кода в Block1, если они действительно будут использоваться в любых измерениях, например, путем вычисления разницы между ними и печати результата в качестве вывода, Такое переупорядочение явно нарушит Java-in-thread as-if-serial семантику. Он изменяет результаты от того, что было бы получено, выполняя инструкции в указанном порядке программы. Если присваивания не используются для каких-либо измерений и не имеют побочных эффектов какого-либо вида на результат программы, они скорее всего будут оптимизированы, как компилятор, а не будут переупорядочены.

Ответ 3

Прежде чем ответить на вопрос,

читает и записывает переменные

Должно быть

волатильные чтения и волатильные записи (одного поля)

Приказ программы не гарантирует, что это происходит до отношения, скорее, отношения "доживет" гарантируют порядок программы

На ваши вопросы:

Означает ли это, что чтение и запись могут быть изменены по порядку, но чтение и запись не могут изменить порядок с действиями, указанными во 2-й или 3-й строках?

Ответ на самом деле зависит от того, какое действие происходит первым и какое действие происходит во втором. Взгляните на JSR 133 Cookbook для компиляторов Writers. Существует сетка Can Reorder, в которой указано разрешенное переупорядочение компилятора, которое может произойти.

Например, Неактивное хранилище можно переупорядочить выше или ниже Обычное хранилище, но Неактивное хранилище нельзя переупорядочить выше или ниже Volatile Load. Все это предполагает, что семантика intrathread сохраняется.

Что означает "порядок программы"?

Это из JLS

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

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

Например

public static Object getInstance(){
    if(instance == null){
         instance = new Object();
    }
    return instance;
}

Можно изменить порядок на

public static Object getInstance(){
     Object temp = instance;
     if(instance == null){
         temp = instance = new Object();
     }
     return temp;
}

Ответ 4

это просто означает, что поток может быть многократным, но внутренний порядок действия/операции/инструкции потока останется постоянным (относительно)

thread1: T1op1, T1op2, T1op3... thread2: T2op1, T2op2, T2op3...

хотя порядок работы (Tn'op'M) между потоком может меняться, но операции T1op1, T1op2, T1op3 внутри потока всегда будут в этом порядке, а так как T2op1, T2op2, T2op3

для ex:

T2op1, T1op1, T1op2, T2op2, T2op3, T1op3

Ответ 5

Учебник по Java http://docs.oracle.com/javase/tutorial/essential/concurrency/memconsist.html говорит, что происходит - до того, как отношения - это просто гарантия того, что память, записанная одним конкретным оператором, видна другому конкретному утверждению, Вот иллюстрация

int x;

synchronized void x() {
    x += 1;
}

synchronized void y() {
    System.out.println(x);
}

synchronized создает связь между событиями, если мы удалим его, не будет никакой гарантии, что после того, как поток A будет увеличивать x, поток B будет печатать 1, он может печатать 0