Использование ключевого слова volatile с изменяемым объектом

Я понимаю, что в Java ключевое слово volatile обеспечивает видимость переменных. Вопрос в том, если переменная является ссылкой на изменяемый объект, обеспечивает ли volatile видимость членов внутри этого объекта?

В приведенном ниже примере все работает правильно, если несколько потоков обращаются к volatile Mutable m и изменяют value?

пример

class Mutable {
    private int value;
    public int get()
    {
        return a;
    }
    public int set(int value)
    {
        this.value = value;
    }
}

class Test {
    public volatile Mutable m;
}

person Hongbo    schedule 06.01.2011    source источник
comment
Для сравнения вы можете взглянуть на AtomicReference, который делает это правильно и является встроенным.   -  person Peter Lawrey    schedule 06.01.2011
comment
Если вы постоянно читаете только Test.m, то отношения происходит до вообще не устанавливаются.   -  person Tom Hawtin - tackline    schedule 06.01.2011


Ответы (5)


Это своего рода объяснение некоторых деталей volatile. Пишу это здесь, потому что это слишком много для комментария. Я хочу привести несколько примеров, которые показывают, как volatile влияет на видимость и как это изменилось в jdk 1.5.

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

public class MyClass
{
  private int _n;
  private volatile int _volN;

  public void setN(int i) {
    _n = i;
  }
  public void setVolN(int i) {
    _volN = i;
  }
  public int getN() { 
    return _n; 
  }
  public int getVolN() { 
    return _volN; 
  }

  public static void main() {
    final MyClass mc = new MyClass();

    Thread t1 = new Thread() {
      public void run() {
        mc.setN(5);
        mc.setVolN(5);
      }
    };

    Thread t2 = new Thread() {
      public void run() {
        int volN = mc.getVolN();
        int n = mc.getN();
        System.out.println("Read: " + volN + ", " + n);
      }
    };

    t1.start();
    t2.start();
  }
}

Поведение этого тестового кода четко определено в jdk1.5+, но не четко определено до jdk1.5.

В мире до jdk1.5 не было определенной связи между доступом к энергозависимым и энергонезависимым доступам. поэтому вывод этой программы может быть:

  1. Чтение: 0, 0
  2. Чтение: 0, 5
  3. Читать: 5, 0
  4. Читать: 5, 5

В мире jdk1.5+ семантика volatile была изменена, так что доступ к volatile влияет на доступ к энергонезависимому точно так же, как синхронизация. поэтому в мире jdk1.5+ возможны только определенные выходные данные:

  1. Чтение: 0, 0
  2. Чтение: 0, 5
  3. Чтение: 5, 0 ‹- невозможно
  4. Читать: 5, 5

Выход 3. невозможен, поскольку чтение «5» из volatile _volN устанавливает точку синхронизации между двумя потоками, что означает, что все действия из t1, предпринятые до назначения _volN, должны быть видимыми для t2 .

Дальнейшее чтение:

person jtahlborn    schedule 07.01.2011
comment
Хорошо, без volatile существует следующее отношение: hb(w_n, w_volN), hb(r_volN,r_n). Если вы добавите volatile, может быть добавлено hb(w_volN,r_volN). По транзитивности теперь hb(w_n,r_n). Правило транзитивности не зависит от семантики volatile. - person OrangeDog; 08.01.2011
comment
Я не убежден, что эта семантика не работает до версии 1.5. Они могут быть неявными, но все же должны быть четко определены. - person OrangeDog; 08.01.2011
comment
@OrangeDog - может быть, вы поверите Брайану Гетцу, одному из архитекторов изменений модели памяти Java. добавил ссылки на свои статьи. - person jtahlborn; 08.01.2011
comment
Спасибо. Почему вы не могли предоставить эти ссылки для начала? Однако отвлечения на проблемы с реализацией, которые были исправлены 7 лет назад, не очень помогают. Я изменил свой ответ, чтобы избежать небрежного использования слова «синхронизация». - person OrangeDog; 08.01.2011
comment
@OrangeDog — они были самыми популярными в Google, поэтому их было довольно легко найти. Моя цель состояла в том, чтобы показать, что volatile абсолютно влияет на видимость. я пытался показать, как он изменился на примере, доказывающем, что это влияет на видимость. эти ссылки также показывают, как изменчивость влияет на видимость. Таким образом, ваш ответ все еще в лучшем случае вводит в заблуждение, а в худшем - неверен. (на самом деле меня не беспокоило использование вами слова «синхронизация», так как это было единственно правильное утверждение в вашем ответе). - person jtahlborn; 08.01.2011
comment
Привет, @jtahlborn, одно продолжение из будущего. Означает ли невозможность случая 3, что компилятор или jvm не могут изменить порядок назначения независимых переменных, если одна из них изменчива? Если t1 переупорядочивается, чтобы сначала присвоить volatile переменную, то возможен случай 3. - person user2259824; 07.07.2016
comment
@ user2259824 - как я уже говорил, это невозможно на основе семантики памяти jdk 1.5+. - person jtahlborn; 10.07.2016

В вашем примере ключевое слово volatile гарантирует только то, что последняя ссылка, написанная любым потоком на «m», будет видна любому потоку, читающему «m» впоследствии.

Это ничего не гарантирует в отношении вашего get().

Итак, используя следующую последовательность:

Thread-1: get()     returns 2
Thread-2: set(3)
Thread-1: get()    

для вас совершенно законно вернуть 2, а не 3. volatile ничего в этом не меняет.

Но если вы измените свой класс Mutable на это:

class Mutable {
    private volatile int value;
    public int get()
    {
        return a;
    }
    public int set(int value)
    {
        this.value = value;
    }
}

Тогда гарантируется, что второй get() из Thread-1 вернет 3.

Однако обратите внимание, что volatile обычно не лучший метод синхронизации.

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

person Gugussee    schedule 06.01.2011
comment
этот ответ в основном правильный. одно примечание, однако, когда m назначено, внутреннее значение будет правильно отображаться. только после последующих вызовов set(), которые не пишут m, у вас возникают проблемы. - person jtahlborn; 06.01.2011
comment
@jtahlborn, не могли бы вы дать ссылку на это? В JLS не нашел. В нем только говорится, что запись в переменное поле (§8.3.1.4) происходит перед каждым последующим чтением этого поля, но это ничего не говорит о фактической инициализации объекта, на который ссылается поле. - person Sergei Tachenov; 06.01.2011
comment
прочитайте раздел 17. определение «происходит раньше» и 17.4.2 объясните, что volatile теперь эквивалентно синхронизированному с точки зрения семантики памяти. - person jtahlborn; 06.01.2011
comment
@jtahlborn: под внутренним значением вы имеете в виду свойства экземпляра. Поскольку m — это просто ссылка на объект, было бы неправильно говорить, что вызовы set() на самом деле пишут в m. - person Farhan Shirgill Ansari; 20.08.2016
comment
@ShirgillFarhanAnsari - нет, звонки на set() не пишут на m. вы будете читать ссылку m только для того, чтобы вызвать set()`. - person jtahlborn; 21.08.2016
comment
@jtahlborn: это было поднято здесь - stackoverflow.com/questions/39073702/ - person Farhan Shirgill Ansari; 22.08.2016
comment
@ShirgillFarhanAnsari - добавлен ответ на другой ваш вопрос - person jtahlborn; 22.08.2016

volatile предоставляет только гарантии относительно ссылки на объект, объявленный таким образом. Члены этого экземпляра не синхронизируются.

Согласно Википедии, у вас есть:

  • (Во всех версиях Java) Существует глобальный порядок чтения и записи в volatile переменную. Это означает, что каждый поток, обращающийся к изменчивому полю, будет считывать его текущее значение перед продолжением вместо того, чтобы (потенциально) использовать кешированное значение. (Однако нет никакой гарантии относительного порядка чтения и записи volatile с обычными операциями чтения и записи, а это означает, что это, как правило, бесполезная конструкция многопоточности.)
  • (В Java 5 или более поздних версиях) Чтение и запись Volatile устанавливают отношения «происходит до», очень похоже на получение и освобождение мьютекса.

Итак, в основном у вас есть то, что, объявляя поле volatile, взаимодействуя с ним, создается «точка синхронизации», после которой любое изменение будет видно в других потоках. Но после этого использование get() или set() не синхронизируется. Спецификация Java объяснение.

person Mario F    schedule 06.01.2011
comment
это неправильно. вы проигнорировали второй пункт в википедии, где java 5 сделал volatile влияет на энергонезависимые переменные. - person jtahlborn; 06.01.2011
comment
@jtahlborn, вы правы, я добавил второй пункт и немного переформулировал собственное объяснение. - person Mario F; 06.01.2011
comment
ваше объяснение по-прежнему не верно. по-прежнему кажется, что volatile не влияет на энергонезависимые поля. при первом назначении m гарантируется видимость значения гарантируется. см. мой комментарий к сообщению @Gugusee для более подробной информации. - person jtahlborn; 06.01.2011

Использование volatile вместо полного значения synchronized по существу является оптимизацией. Оптимизация исходит из более слабых гарантий, предоставляемых для значения volatile по сравнению с доступом synchronized. Преждевременная оптимизация — корень всех зол; в этом случае зло может быть трудно отследить, потому что оно будет в форме условий гонки и тому подобного. Поэтому, если вам нужно спросить, вы, вероятно, не должны использовать его.

person Raedwald    schedule 06.01.2011

volatile не "обеспечивает видимость". Его единственным эффектом является предотвращение кэширования переменной процессором, что обеспечивает отношение «происходит до» при одновременном чтении и записи. Он не влияет на члены объекта и не обеспечивает никакой синхронизации synchronized блокировки.

Поскольку вы не сказали нам, каково «правильное» поведение вашего кода, на вопрос нельзя ответить.

person OrangeDog    schedule 06.01.2011
comment
На самом деле volatile может влиять на члены объекта (но не на то, как его использует OP). это происходит до того, как отношения действительно обеспечивают гарантии видимости. - person jtahlborn; 06.01.2011
comment
@jtahlborn - Не могли бы вы продемонстрировать, как? По второму пункту; в то время как эффект заключается в том, что записи видны для чтения, термин обеспечивает видимость переменной слишком расплывчат, чтобы иметь смысл. - person OrangeDog; 06.01.2011
comment
@OrangeDog: скажем, вы создали локальный экземпляр Mutable и вызвали метод set() с тремя разными значениями, а затем назначили свой локальный экземпляр Mutable m. все последующие чтения m гарантированно увидят последнее значение, установленное первым потоком до того, как он присвоил m. таким образом, видимость значения гарантируется при назначении m. однако, как правильно указано в другом месте, влияние любых будущих вызовов set() после присвоения m не гарантируется видимым. (где «видимый» означает, что он гарантированно будет виден другим потоком). - person jtahlborn; 06.01.2011
comment
@jtahlborn - это описывает ситуацию, когда затрагивается только объектная переменная (m), а не ее члены. - person OrangeDog; 06.01.2011
comment
@OrangeDog: запись в m обеспечивает гарантию видимости значения. может быть, это кажется вам логичным, поэтому вы не понимаете, что я говорю. однако, если вы посмотрите на семантику volatile до jdk1.5, вы увидите, что это было не так. до jdk1.5 значение не имело абсолютно никаких гарантий. - person jtahlborn; 06.01.2011
comment
Оранжевый говорит, что если вы создадите локальную переменную Mutable t, вызовете набор с некоторым произвольным значением и назначите это локальное поле t для m, тогда да, значение m будет считаться актуальным. - person John Vint; 06.01.2011
comment
@Джон - Ну да, очевидно. Это связано с влиянием volatile на ссылочную переменную m, а не с каким-либо влиянием на элементы объекта. - person OrangeDog; 07.01.2011
comment
Каждая строка кода в одном потоке выполняется раньше следующей. Базовая процедурная семантика гарантирует, что set() произойдет до m = t, и что чтение m произойдет до get(). Семантика volatile гарантирует, что m = t произойдет только до чтения m, если и только если поток записи выполнил запись до того, как поток чтения добрался до чтения. - person OrangeDog; 07.01.2011
comment
@OrangeDog: нет, это неправильно. Заявление @ John (хотя и правильное) сбивало с толку, потому что, когда он сказал значение, он имел в виду поле члена в Mutable. семантика "происходит до" volatile гарантирует, что все действия потока, предпринятые до m = t, видны любому другому потоку, который впоследствии считывает m, включая любое предыдущее присвоение полю значения. Как я уже упоминал в другом комментарии, вы можете увидеть это в разделе 17 jls - volatile имеет ту же семантику памяти, что и синхронизированная. - person jtahlborn; 07.01.2011
comment
@jtahlborn - Нет, я прав. То, что вы говорите, является прямым следствием того, что происходит-прежде чем быть временным отношением (a < b & b < c -> a < c), что должно быть совершенно очевидно, учитывая его название. Это не имеет ничего общего с семантикой volatile. - person OrangeDog; 07.01.2011
comment
@OrangeDog: хех, это одна из классических ловушек многопоточности. я видел этот баг много раз. отношение "происходит до" не гарантируется в двух потоках, если у вас нет точки синхронизации между двумя потоками. volatile обеспечивает эту гарантию. пожалуйста, прочитайте этот материал, прежде чем публиковать больше дезинформации. - person jtahlborn; 07.01.2011
comment
@jtahlborn - именно это я и сказал. Вы тот, кто не может выделить специфическую семантику отдельных особенностей языка. Пожалуйста, прочитайте мои комментарии, прежде чем публиковать больше дезинформации. - person OrangeDog; 07.01.2011
comment
@OrangeDog: я прочитал все ваши комментарии и ваш оригинальный пост. извините, вы все еще не правы. семантика volatile оказывает прямое влияние на отношения между двумя потоками. это явно описано в последней версии jls и представляет собой изменение между версиями до jdk1.5 и jdk1.5+. я могу подтвердить свои слова с помощью jls. можете подтвердить свои утверждения? - person jtahlborn; 07.01.2011
comment
@OrangeDog Каждая строка кода, написанная разработчиком (в соответствии с 1.5+ JMM), не обязательно должна выполняться одна за другой в одном и том же потоке. Это просто так кажется. Последовательная согласованность отличается от линеаризуемости. Когда вы запускаете программу, в которой нет синхронизации, код можно заказать, но это законно. Случается-прежде отношения гарантированы только, как jtalborn, с использованием точек синхронизации. - person John Vint; 07.01.2011
comment
@John V. - Если x и y являются действиями одного и того же потока и x предшествует y в порядке программы, то hb (x, y). - java.sun.com/docs/books/jls /третье_издание/html/ - person OrangeDog; 07.01.2011
comment
@jtahlborn - я не не согласен с этим, я не согласен с тем, что то, что я сказал, неверно. - person OrangeDog; 07.01.2011
comment
Это отличается от того, что каждая строка кода происходит перед следующей. - person John Vint; 07.01.2011
comment
@ Джон В. - происходит раньше или происходит раньше? Это очень важное различие. - person OrangeDog; 07.01.2011
comment
Даже раньше в терминологии специально не использовалась фраза «каждая строка кода происходит до следующей строки», потому что это неверно. Конкретное действие, кажется, происходит перед любым последующим действием, но не имеет отношения к строке кода, в которой оно было выполнено. - person John Vint; 07.01.2011
comment
@John V. - x стоит перед y в порядке программы, относится к строке кода, в которой действия появляются в программе. hb(x, y) означает, что x происходит раньше, чем y. Порядок выполнения может быть любым; это взаимосвязь между строками кода (порядок программы) и событиями, которые произошли раньше. - person OrangeDog; 07.01.2011
comment
Используем в качестве крайнего примера. У вас есть в классе final int j; конечный инт я; публичный SoemConstructor(){ j = 10; я =10; } Ничто не помешает компилятору переместить записи j и i в be: final int j,i =10; Одна и та же строка кода — строки кода по уважительной причине не должны использоваться для определения последовательной согласованности. - person John Vint; 07.01.2011
comment
@Джон В. - Как показывает ваш пример (и я только что сказал), случается - до - это не то же самое, что порядок выполнения. Если вместо этого код был бы i = 10 + f(j), то компилятор не смог бы переставить порядок выполнения, не нарушая «происходит до». (Вероятно, его можно было бы оптимизировать до f(10).) - person OrangeDog; 07.01.2011
comment
Да, это правильно. Я хотел сказать, что порядок программы определяется не строками кода, а действиями ОС/JVM, которые имеют место. - person John Vint; 07.01.2011
comment
Ниже добавлен отдельный ответ, потому что я не мог написать пример в комментарии. @OrangeDog: пожалуйста, прокомментируйте мой ответ и дайте мне знать, с чем вы не согласны. - person jtahlborn; 07.01.2011
comment
@John V. - строки кода == порядок программы!= порядок выполнения - person OrangeDog; 08.01.2011