Почему Unsafe.fullFence () не обеспечивает видимость в моем примере? - PullRequest
2 голосов
/ 11 января 2020

Я пытаюсь углубиться в ключевое слово volatile в Java и настроить 2 среды тестирования. Я считаю, что они оба с x86_64 и используют точку доступа.

Java version: 1.8.0_232
CPU: AMD Ryzen 7 8Core

Java version: 1.8.0_231
CPU: Intel I7

Код здесь:

import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class Test {

  private boolean flag = true; //left non-volatile intentionally
  private volatile int dummyVolatile = 1;

  public static void main(String[] args) throws Exception {
    Test t = new Test();
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);

    Thread t1 = new Thread(() -> {
        while (t.flag) {
          //int b = t.someValue;
          //unsafe.loadFence();
          //unsafe.storeFence();
          //unsafe.fullFence();
        }
        System.out.println("Finished!");
      });

    Thread t2 = new Thread(() -> {
        t.flag = false;
        unsafe.fullFence();
      });

    t1.start();
    Thread.sleep(1000);
    t2.start();
    t1.join();
  }
}

"Закончено!" никогда не печатается, что не имеет смысла для меня. Я ожидаю, что fullFence в потоке 2 делает flag = false глобально видимым.

Из моего исследования Hotspot использует lock/mfence для реализации fullFence на x86. И в соответствии с Запись справочного руководства по набору инструкций Intel для mfence

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

Еще «хуже», если я закомментирую fullFence в потоке 2 и откомментирую любой из xxxFence в потоке 1, код распечатывает "Готово!" Это имеет еще меньше смысла, потому что по крайней мере lfence является «бесполезным» / no-op в x86 .

Возможно, мой источник информации содержит неточность или я что-то неправильно понимаю. Пожалуйста, помогите, спасибо!

1 Ответ

3 голосов
/ 11 января 2020

Важно не эффект забора, а эффект времени компиляции, заставляющий компилятор перезагружать вещи.

Ваш t1 l oop не содержит volatile операций чтения или чего-либо еще, что могло бы синхронизироваться с другим потоком, поэтому нет гарантии, что он когда-либо заметит любые изменения любых переменных. т.е. при JITing в asm компилятор может сделать al oop, который загружает значение в регистр один раз, вместо того, чтобы каждый раз перезагружать его из памяти. Это тот тип оптимизации, который вы всегда хотите, чтобы компилятор мог выполнять для не общих данных, поэтому в языке есть правила, которые позволяют ему это делать, когда невозможна синхронизация.

И, конечно же, условие можно поднять из л oop. Так что без барьеров или чего-либо, ваш читатель l oop может JIT в asm, который реализует эту логику c:

if(t.flag) {
   for(;;){}  // infinite loop
}

Помимо упорядочения, другая часть Java volatile - это предположение о том, что другие потоки могут изменять его асинхронно, поэтому нельзя считать, что множественные чтения дают одно и то же значение.

Но unsafe.loadFence(); делает перезагрузку JVM t.flag из (кэш-когерентной Запоминание каждой итерации. Я не знаю, требуется ли это Java spe c или просто деталью реализации, которая заставляет его работать.

Если это был C ++ с переменной не atomic (которая было бы неопределенное поведение в C ++), вы бы увидели точно такой же эффект в компиляторе, как G CC. _mm_lfence также будет полным барьером во время компиляции, а также выдаст бесполезную инструкцию lfence, эффективно сообщающую компилятору, что вся память могла измениться и, следовательно, должна быть перезагружена. Поэтому он не может переупорядочивать нагрузки или выводить их из циклов.

Кстати, я бы не был уверен, что unsafe.loadFence() даже JIT соответствует инструкции lfence на x86. Это бесполезно для упорядочения памяти (за исключением очень непонятных вещей, таких как ограждение загрузок NT из памяти W C, например, копирование из видеопамяти RAM, что JVM может предположить, что не происходит), поэтому JITM JITM для x86 это можно рассматривать как барьер времени компиляции. Точно так же, как компиляторы C ++ делают для std::atomic_thread_fence(std::memory_order_acquire); - переупорядочение блоков во время компиляции нагрузок через барьер, но не выдает asm-инструкций, потому что asm-память хоста, на котором работает JVM, уже достаточно сильна.


В теме 2 unsafe.fullFence(); я считаю бесполезным . Это просто заставляет поток ждать, пока более ранние хранилища не станут глобально видимыми, прежде чем могут произойти более поздние загрузки / хранилища. t.flag = false; - это видимый побочный эффект, который нельзя оптимизировать, поэтому в асимметричном JIT-файле определенно происходит, есть ли за ним барьер или нет, даже если это не volatile. И его нельзя отложить или объединить с чем-то другим, потому что в этом же потоке больше ничего нет.

Хранилища Asm всегда становятся видимыми для других потоков, вопрос только в том, ждет ли текущий поток своего буфера хранения, чтобы слить или нет, прежде чем делать больше вещей (особенно нагрузок) в этой теме. т.е. предотвратить все переупорядочения, включая StoreLoad. Java volatile делает это, как C ++ memory_order_seq_cst (используя полный барьер после каждого магазина), но без барьера это все еще магазин как C ++ memory_order_relaxed. (Или при JIT для x86 asm нагрузки / хранилища на самом деле так же сильны, как и получение / освобождение.)

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


Предупреждение: я не знаю много Java, и я точно не знаю, насколько небезопасно / неопределенно назначать не- volatile в одном потоке и читайте в другом без синхронизации. В зависимости от поведения, которое вы видите, это звучит точно так же, как и в C ++ для того же, что и с переменными, отличными от atomic (с включенной оптимизацией, как всегда делает HotSpot)

(на основе @ Комментарий Маргарет, я обновил некоторые догадки о том, как я предполагаю, что Java синхронизация работает. Если я что-то неправильно указал, пожалуйста, отредактируйте или прокомментируйте.)

В C ++ гонки данных на не- atomic переменных всегда являются неопределенным поведением, но, конечно, при компиляции для реальных ISA (которые не работают аппаратно профилактика расы) результаты иногда являются желаемыми.

...