Java, энергозависимые и памяти барьеры на архитектуре x86

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

Язык - это Java.

Из того, что я до сих пор понимал, в архитектуре X86 барьеры StoreLoad (несмотря на точные инструкции ЦП, используемые для их реализации) ставятся после записи Volatile, чтобы сделать их видимыми для последующих чтений Volatile в других потоках (поскольку x86 не гарантирует что новые чтения всегда видят старые записи) (ссылка http://shipilev.net/blog/2014/on-the-fence-with-dependencies/)

Теперь отсюда ( http://jpbempel.blogspot.it/2013/05/volatile-and-memory-barriers.html) я вижу, что:

public class TestJIT
{
    private volatile static int field1;
    private static int field2;
    private static int field3;
    private static int field4;
    private static int field5;
    private volatile static int field6;

    private static void assign(int i)
    {
        field1 = i << 1; // volatile
        field2 = i << 2;
        field3 = i << 3;
        field4 = i << 4;
        field5 = i << 5;
        field6 = i << 6; // volatile.
    }

    public static void main(String[] args) throws Exception
    {
        for (int i = 0; i < 10000; i++)
        {
            assign(i);
        }
        Thread.sleep(1000);
    }
}

результирующая сборка имеет StoreLoad только после назначения field6, а не после назначения field1, что также является изменчивым.

Мои вопросы:

1) Имеет ли смысл то, что я написал до сих пор? Или я полностью что-то неправильно понимаю?

2) Почему компилятор пропускает StoreLoad после энергозависимого назначения field1? Это оптимизация? Но есть ли у него недостатки? Например, другой поток запускается после назначения field1, может все еще прочитать старое значение для field1, даже если оно было фактически изменено?

3 ответа

1) Имеет ли смысл то, что я написал до сих пор? Или я полностью что-то неправильно понимаю?

Я думаю, что вы все правильно поняли.

2) Почему компилятор пропускает StoreLoad после энергозависимого назначения field1? Это оптимизация? Но есть ли у него недостатки?

Да, это оптимизация, но это довольно сложно сделать правильно.

Поваренная книга Дуга Ли JMM фактически показывает пример рекомендуемых барьеров в случае двух последовательных volatile магазины, и есть StoreLoad после каждого из них есть StoreStore (x86 no-op) между двумя магазинами и StoreLoad только после второго. Поваренная книга однако отмечает, что связанный анализ может быть довольно вовлечен.

Компилятор должен быть в состоянии доказать, что volatile чтение не может происходить в порядке синхронизации между записью в field1 и написать field6, Я не уверен, что это выполнимо (текущим JIT HotSpot), если TestJIT был немного изменен, так что сопоставимое количество volatile загрузка выполняется в другом потоке одновременно.

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

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

Обновить

Изменены подробности в примере с JMM Cookbook, так как kRs указали, что я ошибся StoreStore для StoreLoad, Суть ответа не изменилась вообще.

Почему компилятор пропускает StoreLoad после энергозависимого назначения field1?

Только первая загрузка и последнее хранилище должны быть нестабильными.

Это оптимизация?

Если это происходит, это наиболее вероятная причина.

Но есть ли у него недостатки?

Только вы полагаетесь на то, что существует два магазина барьер. т.е. тебе нужно увидеть field1 изменилось раньше field6 изменился чаще, чем это произошло бы случайно.

может все еще прочитать старое значение для field1, даже если оно было фактически изменено?

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

Чтобы ответить на вопрос (1), вы правы в отношении всего, что вы сказали о барьерах памяти и т. Д. (Хотя объяснение неполное. Барьер памяти обеспечивает порядок ВСЕХ загрузок / сохранений перед ним, а не только энергозависимых). Пример кода ненадежный, хотя.

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

Рассмотрим этот пример;

public void thread1()
{
    //no assurances about ordering
    counter1 = someVal; //some non-volatile store
    counter2 = someVal; //some non-volatile store
}

public void thread2()
{
    flag += 1; //some volatile operation

    System.out.println(counter1);
    System.out.println(counter2);
}

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

Чтобы это исправить, нам нужно упорядочить записи в thread1 с барьером памяти (он же volatile операция);

public void thread1()
{
    counter1 = someVal; //some non-volatile store
    counter2 = someVal; //some non-volatile store

   //now we use a volatile write 
   //this ensures the order of our writes
   flag = true; //volatile operation

}

public void thread2()
{
   //thread1 has already ordered the memory operations (behind the flag)
   //therefore we don't actually need another memory barrier here
   if (flag)
   {
       //both counters have the right value now
   }
}

В этом примере порядок обрабатывается thread1, но зависит от состояния флага. Таким образом, нам нужно только проверить состояние flag, но вам не нужен еще один барьер памяти для этого чтения (иначе вам нужно проверить изменяемое поле, просто барьер памяти не нужен).

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

Другие вопросы по тегам