Переопределение чтения

Предположим, что есть два потока без синхронизации, один устанавливает n = 1 другой выполняет method().

В следующем "read" всегда ссылается на чтение поля n.

public class MyClass
{
  public int n = 0;

  public void method() {
    System.out.println(n); //read 1
    System.out.println(n); //read 2
  }
}

Будет ли возможен следующий вывод?

1
0

Ответ да, потому что хотя чтение 1 происходит - до чтения 2, тем не менее, возможно, чтобы чтение 2 было переупорядочено перед чтением 1, потому что оно не изменило бы семантику внутрипоточного исполнения.

Правильно ли это рассуждение?

Ответы

Ответ 1

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

например. В JLS 17.4.5 говорится:

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

Гонки данных жутко выглядят так: racy reads может возвращать удивительные данные по каждому считыванию, а модель памяти Java фиксирует это. Таким образом, более точный ответ заключается в том, что выполнение, которое производит (1, 0), не нарушает ограничений модели памяти Java (согласованность порядка синхронизации, порядок синхронизации - последовательность последовательности программ, происходит до согласованности, требования к причинности) и поэтому разрешено.

Реализация: на оборудовании обе нагрузки могут быть запущены и/или поступать в подсистему памяти в разное время, независимо от их "порядка программы", поскольку они независимы; в компиляторах планирование команд также может игнорировать порядок работы программы для независимых чтений, подвергая нагрузку аппаратным средствам в "контр-интуитивном" порядке.

Если вы хотите, чтобы читал в заказе программы, вам нужно более сильное свойство. JMM предоставляет это свойство для действий синхронизации (в вашем примере это делает переменную volatile), которая связывает действия в общем порядке синхронизации, соответствующие порядку программы. В этом случае (1, 0) будет запрещено.

Иллюстрация на очень специальном тестовом сценарии jcstress (см. полный источник для оговорок):

private final Holder h1 = new Holder();
private final Holder h2 = h1;

private static class Holder {
    int a;
    int trap;
}

@Actor
public void actor1() {
    h1.a = 1;
}

@Actor
public void actor2(IntResult2 r) {
    Holder h1 = this.h1;
    Holder h2 = this.h2;
    h1.trap = 0;
    h2.trap = 0;
    r.r1 = h1.a;
    r.r2 = h2.a;
}

Даже на x86, который не меняет порядок загрузки, выдает (1, 0), oops:

      [OK] o.o.j.t.volatiles.ReadAfterReadTest                                                                                                      
    (fork: #1, iteration #1, JVM args: [-server])
  Observed state   Occurrences              Expectation  Interpretation                                              
          [0, 0]    16,736,450               ACCEPTABLE  Doing both reads early.                                     
          [1, 1]   108,816,262               ACCEPTABLE  Doing both reads late.                                      
          [0, 1]         3,941               ACCEPTABLE  Doing first read early, not surprising.                     
          [1, 0]        84,477   ACCEPTABLE_INTERESTING  First read seen racy value early, and the s...

Создание Holder.a volatile сделает (1, 0), чтобы уйти.

Ответ 2

У нас есть 4 действия, которые формируют следующее: перед графиком:

+-------+     ?    +-------+
| n = 0 |   ---->  | n = 1 |
+-------+          +-------+
    |
    |?
    v
  +---+             +---+
  | n |     ---->   | n |
  +---+             +---+

Так как вы не указываете код, инициализирующий n, неизвестно, происходит ли n = 0 - до n = 1 и выполняется ли n = 0 - до первого чтения n.

Если эти ребра не существуют, (n = 1, n, n = 0, n) является последовательным последовательным порядком выполнения, а выход 1 0 тривиально возможен.

Если известно, что n = 0 происходит до n = 1, последовательное последовательное выполнение с выходом 1 0.

Однако спецификация языка Java гарантирует, что все исполнения последовательно согласованы, если они свободны от гонок данных, которых нет в нашей программе. В частности, spec пишет:

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

И

Мы говорим, что чтению r переменной v разрешено наблюдать запись w в v, если в частичном порядке выполнения:

  • r не упорядочивается до w (т.е. это не тот случай, когда hb (r, w)) и

  • нет промежуточной записи w 'в v (т.е. не писать w' в v, что hb (w, w ') и hb (w', r)).

В нашем случае оба чтения имеют возможность наблюдать как 0, так и 1, потому что нет промежуточной записи.

Поэтому, насколько я могу судить, выход 1 0 разрешен Спецификацией языка Java.