Модель памяти Java: смешивание несинхронизировано и синхронизировано

Предположим, что существует такой класс:

public void MyClass {
    private boolean someoneTouchedMeWhenIWasWorking;

    public void process() {
        someoneTouchedMeWhenIWasWorking = false;
        doStuff();
        synchronized(this) {
            if (someoneTouchedMeWhenIWasWorking) {
                System.out.println("Hey!");
            }
        }
    }

    synchronized public void touch() {
        someoneTouchedMeWhenIWasWorking = true;
    }
}

Один поток вызывает process, другой вызывает touch. Обратите внимание, как process очищает флаг без синхронизации при его запуске.

С моделью памяти Java, возможно ли, что поток, выполняющий process, увидит эффект своей локальной, несинхронизированной записи, даже если touch произошло позже?

То есть, если потоки выполняются в следующем порядке:

T1: someoneTouchedMeWhenIWasWorking = false; // Unsynchronized
T2: someoneTouchedMeWhenIWasWorking = true; // Synchronized
T1: if (someoneTouchedMeWhenIWasWorking) // Synchronized

... возможно ли, что T1 увидит значение из своего локального кеша? Может ли он сбросить то, что он имеет в памяти (переопределив все, что написал T2), и перезагрузить это значение из памяти?

Нужно ли синхронизировать первую запись или сделать переменную изменчивой?

Я был бы очень признателен, если бы ответы были подкреплены документацией или некоторыми респектабельными источниками.

Ответ 1

Причины JLS в терминах произойти - до отношений (hb). Пусть аннотируется чтение и запись вашего предполагаемого исполнения, используя соглашение JLS:

T1: someoneTouchedMeWhenIWasWorking = false;   // w
T2: someoneTouchedMeWhenIWasWorking = true;    // w'
T1: if (someoneTouchedMeWhenIWasWorking)       // r

Мы имеем следующее: перед отношениями:

  • hb (w, r), из-за правила порядка программы
  • hb (w ', r), из-за synchronized ключевое слово (разблокировка на мониторе происходит - перед каждой последующей блокировкой этот монитор): если ваша программа выполняет w 'до r (глядя на ваши часы), которую вы предположили, тогда hb (w', r)

Ваш вопрос в том, разрешено ли r читать w. Это описано в в последнем разделе # 17.4.5:

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

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

Неофициально, чтению r разрешено видеть результат записи w, если этого не происходит - перед порядком, чтобы предотвратить чтение.

  • Первое условие ясно выполнено: мы определили, что hb (w, r).
  • Второе условие также верно, потому что не происходит - до отношения между w и w '

Итак, если я ничего не пропустил, кажется, что r теоретически может наблюдать либо w, либо w '.

На практике, как указывалось другими ответами, типичные JVM реализованы таким образом, что более строгие, что JMM и я не думаю, что какая-либо текущая JVM позволит r наблюдателю w, но это не гарантия.

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

Ответ 2

С моделью памяти Java, возможно ли, что процесс выполнения потока увидит эффект своей локальной, несинхронизированной записи, хотя касание произошло позже?

Я вижу здесь тонкое, но важное упрощение: как, по-вашему, вы бы определили, что действие записи вторым потоком произошло точно между действиями записи и чтения первым потоком? Чтобы иметь возможность сказать это, вам нужно будет предположить, что действие записи является идеализированной точкой на временной линии и что все записи происходят в последовательном порядке. На самом аппаратном обеспечении запись представляет собой очень сложный процесс, включающий в себя конвейер процессора, хранилища буферов, кеш L1, кэш второго уровня, шину на передней панели и, наконец, ОЗУ. Такой же процесс происходит одновременно на всех ядрах ЦП. Итак, что именно вы подразумеваете под одной записью "происходит после" другой?

Кроме того, рассмотрим парадокс Roach Motel, который, по-видимому, помогает многим людям как своего рода" умственный ярлык" в последствиях модели памяти Java: ваш код можно юридически преобразовать в

public void process() {
    doStuff();
    synchronized(this) {
        someoneTouchedMeWhenIWasWorking = false;
        if (someoneTouchedMeWhenIWasWorking) {
            System.out.println("Hey!");
        }
    }
}

Это совершенно другой подход к использованию свобод, предоставляемых моделью памяти Java, для повышения производительности. Чтение внутри if-предложения действительно потребует считывания фактического адреса памяти, а не прямой привязки значения false и, следовательно, удаления всего if-блока (это действительно произойдет без synchronized), но запись false не обязательно будет записываться в ОЗУ. На следующем этапе рассуждения оптимизационный компилятор может решить полностью удалить присвоение false. В зависимости от особенностей всех других частей кода это одна из вещей, которые могут произойти, но есть много других возможностей.

Главное сообщение, которое нужно убрать из вышеизложенного, должно быть: не притворяйтесь, что можете уклониться от кода Java, используя некоторую упрощенную концепцию "локального кэширования"; вместо этого придерживайтесь официальной модели памяти Java и рассматривайте все доступные ей свободы.

Нужно ли синхронизировать первую запись или сделать переменную изменчивой?

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

Наконец, представьте, что ваша переменная изменчива. Что это даст вам, что у вас нет сейчас? В терминах формализма JMM две записи будут иметь определенный порядок между ними (введенный общим порядком синхронизации), но вы будете в том же положении, что и вы сейчас: определенный порядок будет произвольным при любом конкретном выполнении.

Ответ 3

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

Это хороший вариант использования изменчивой переменной. "someoneTouchedMeWhenIWasWorking" должен по крайней мере быть переменным.

В вашем примере кода синхронизированный используется для защиты "someoneTouchedMeWhenIWasWorking", который является не чем иным, как флагом. Если он сделан как изменчивый, "синхронизированный" блок/метод можно удалить.

Ответ 4

Чтобы объяснить, когда значение обновлено, позвольте мне привести цитату из этого JSR-133 FAQ:

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

Следовательно, значение someoneTouchedMeWhenIWasWorking публикуется в основной памяти, когда один поток выходит из блока synchonized. Опубликованное значение затем считывается из основной памяти при входе в блоки synchronized.

Вне блоков synchronized значение может быть кэшировано. Но при входе в синхронизированный блок он будет считываться из основной памяти и будет опубликован при выходе из него, даже если он не объявлен volatile.


В этом вопросе есть интересный ответ: Что может заставить обновить нелетучую переменную?

Далее следуют беседы: http://chat.stackoverflow.com/rooms/46867/discussion-between-gray-and-voo

Обратите внимание, что в последнем примере @SotiriosDelimanolis пытается сделать следующее: synchronized(new Object()) {} в блоке while (! stop), но это не остановилось. Он добавил, что с synchronized(this), это произошло. Я думаю, что это поведение объясняется в FAQ, когда оно заявляет:

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