Подтвердить что ты не робот

Поведение барьера памяти в Java

После прочтения большего количества блогов/статей и т.д., я теперь действительно смущен о поведении загрузки/хранения до/после барьера памяти.

Ниже приводятся 2 цитаты из Дуга Ли в одной из его разъяснительной статьи о JMM, которые оба очень тяжелы:

  • Все, что было видимо для потока A, когда оно записывает в volatile field f, становится видимым для потока B, когда он читает f.
  • Обратите внимание, что важно, чтобы оба потока обращались к одной и той же изменчивой переменной, чтобы правильно настроить связь между событиями. Дело не в том, что все, что видимо для потока A, когда оно записывает изменчивое поле f, становится видимым для потока B после того, как оно считывает изменчивое поле g.

Но тогда, когда я просмотрел еще один blog об уровне памяти, я получил следующее:

  • Указатель "sfence" на x86 хранит барьер, заставляя все инструкции хранить перед барьером, который должен произойти перед барьером, и буферы хранилища сброшены в кеш для процессора, на котором он выдается.
  • Нагрузочный барьер, инструкция "lfence" на x86, заставляет все инструкции по загрузке после того, как барьер должен произойти после барьера, а затем подождите, пока буфер загрузки будет стекать для этого CPU.

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

Учитывая следующие коды:

  public class MemoryBarrier {
    volatile int i = 1, j = 2;
    int x;

    public void write() {
      x = 14; //W01
      i = 3;  //W02
    }

    public void read1() {
      if (i == 3) {  //R11
        if (x == 14) //R12
          System.out.println("Foo");
        else
          System.out.println("Bar");
      }
    }

    public void read2() {
      if (j == 2) {  //R21
        if (x == 14) //R22
          System.out.println("Foo");
        else
          System.out.println("Bar");
      }
    }
  }

Скажем, у нас есть 1 поток записи TW1, сначала вызываем метод writeB (запись) MemoryBarrier, тогда у нас есть 2 потока чтения TR1 и TR2, вызывающие метод MemoryBarrier read1() и read2(). Рассматривайте эту программу на CPU, который не сохраняет (x86 DO сохраняет порядок для таких случаев, что не так), в соответствии с моделью памяти будет существовать барьер StoreStore (скажем, SB1) между W01/W02, а также 2 барьер LoadLoad между R11/R12 и R21/R22 (скажем RB1 и RB2).

  • Поскольку SB1 и RB1 находятся на одном мониторе i, поэтому поток TR1, который вызывает read1, должен всегда видеть 14 на x, также всегда печатается "Foo".
  • SB1 и RB2 находятся на разных мониторах, если Doug Lea верен, нить TR2 не будет гарантированно видеть 14 на x, что означает, что "Bar" может печататься иногда. Но если барьер памяти работает, как Мартин Томпсон, описанный в blog, барьер Store будет выталкивать все данные в основную память, а барьер нагрузки будет вытаскивать все данные из основной памяти в cache/buffer, тогда TR2 также будет гарантированно видеть 14 на x.

Я не уверен, какой из них правильный, или оба они но то, о чем говорил Мартин Томпсон, только для архитектуры x86. JMM не гарантирует, что изменение на x видимо для TR2, но реализация x86 делает.

Спасибо ~

4b9b3361

Ответ 1

Дуг Ли прав. Вы можете найти соответствующую часть в разделе §17.4.4 спецификации языка Java:

§17.4.4 Заказ синхронизации

[..] Запись в изменчивую переменную v (§8.3.1.4) синхронизируется со всеми последующими чтениями v любым потоком (где "последующее" определяется в соответствии с порядком синхронизации). [..]

Модель памяти конкретной машины не имеет значения, поскольку семантика языка программирования Java определяется в терминах абстрактной машины - независимой от конкретной машины. Это ответственность среды выполнения Java для выполнения кода таким образом, что она соответствует гарантиям, предоставленным Спецификацией языка Java.


Относительно актуального вопроса:

  • Если дальнейшая синхронизация отсутствует, метод read2 может печатать "Bar", потому что read2 может быть выполнен до write.
  • Если есть дополнительная синхронизация с CountDownLatch, чтобы убедиться, что read2 выполняется после write, тогда метод read2 никогда не будет печатать "Bar", потому что синхронизация с CountDownLatch удаляет данные расы на x.

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

Имеет ли смысл, что запись в переменную volatile не синхронизируется - с чтением любой другой изменчивой переменной?

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

Это на самом деле важно на практике. Давайте сделаем пример. Следующий класс использует изменчивую переменную-член:

class Int {
    public volatile int value;
    public Int(int value) { this.value = value; }
}

Представьте, что этот класс используется только локально внутри метода. JIT-компилятор может легко обнаружить, что объект используется только в этом методе (Анализ Escape).

public int deepThought() {
    return new Int(42).value;
}

С приведенным выше правилом компилятор JIT может удалить все эффекты volatile чтения и записи, потому что переменная volatile недоступна из любого другого потока.

Эта оптимизация фактически существует в компиляторе Java JIT:

Ответ 2

Насколько я понял, на самом деле речь идет о неустойчивом чтении/записи и о его возникновении - перед гарантиями. Говоря об этой части, мне нужно добавить только одно сообщение:

Волатильная запись не может быть перемещена до нормальной записи, волатильные чтения не могут быть перемещены после нормального чтения. Поэтому результаты read1() и read2() будут записываться как nosid.

Говоря о барьерах - определение отлично подходит для меня, но единственное, что, вероятно, смущает вас, это то, что это вещи/инструменты/способ/механизм (назовите его, как вам нравится), чтобы реализовать поведение, описанное в JMM в точке доступа. При использовании Java вы должны полагаться на гарантии JMM, а не на детали реализации.