Летучие семантики по отношению к другим полям

Предположим, что у меня есть следующий код

private volatile Service service;

public void setService(Service service) {
  this.service = service;
}

public void doWork() {
  service.doWork();
}

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

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

Означает ли это, что правильный код правильный?

private volatile boolean serviceReady = false;
private Service service;

public void setService(Service service) {
  this.service = service;
  this.serviceReady = true;
}

public void doWork() {
  if ( serviceReady ) {
    service.doWork();
  }
}

Ответ 1

Да, этот код является "правильным", как он есть, начиная с Java 1.5.

Атоматичность не вызывает беспокойства, с или без volatile (записи на ссылки на объекты являются атомарными), поэтому вы можете в любой момент перечеркнуть это из списка проблем - единственным открытым вопросом является видимость изменений и "правильность" упорядочение.

Любая запись в изменчивую переменную устанавливает связь "произойдет-до" (ключевая концепция новой модели памяти Java, как указано в JSR-133) с любыми последующими чтениями одной и той же переменной. Это означает, что поток чтения должен иметь видимость во всем видимом для записывающего потока: то есть он должен видеть все переменные с хотя бы их "текущими" значениями во время записи.

Мы можем подробно объяснить это, посмотрев раздел 17.4.5 Спецификации Java Language, в частности следующие ключевые моменты:

  • "Если x и y являются действиями одного и того же потока, а x - до y в программном порядке, то hb (x, y)" (т.е. действия в одном потоке не могут быть переупорядочены таким образом, чтобы они были несовместимы с порядок программы)
  • "Запись в изменчивое поле (§8.3.1.4) происходит до каждого последующего чтения этого поля". (это поясняет текст, объясняя, что чтение-считывание волатильного поля является точкой синхронизации)
  • "Если hb (x, y) и hb (y, z), то hb (x, z)" (транзитивность случается раньше)

Итак, в вашем примере:

  • запись в "сервис" (а) происходит до того, как запись в "serviceReady" (b), из-за правила 1
  • запись в "serviceReady" (b) происходит - перед чтением того же (c) из-за правила 2
  • поэтому, (a) происходит до (c) (3-е правило)

означает, что вам гарантировано, что "service" установлен правильно, в этом случае, если serviceReady истинно.

Вы можете увидеть хорошие записи, используя почти тот же пример, один в IBM DeveloperWorks - см. "Новые гарантии для летучих",

Значения

которые были видны А в момент написания V, теперь гарантированы, чтобы быть видимыми для B.

и один в часто задаваемые вопросы JSR-133, написанные авторами этого JSR:

Таким образом, если читатель видит значение true для v, также гарантируется, что запись в 42 произошла до него. Это не было бы правдой при старой модели памяти. Если v не были волатильными, то компилятор мог изменить порядок записи в записи, а чтение считывателя x могло видеть 0.

Ответ 2

AFAIK это правильный код.

@CPerkins: включение синхронного метода setService не будет работать, так как вам также нужно синхронизировать при чтении.

Однако в этом случае одной переменной будет достаточно. Зачем вам нужно дополнительное логическое поле. Например.

private volatile Service service;

public void setService(Service service) {
  this.service = service;
}

public void doWork() {
  if ( service != null ) {
    service.doWork();
  }
}

учитывая, что никто не вызывает setService для null. Поэтому вы должны, вероятно, проверить нуль:

private volatile Service service;

public void setService(Service service) {
  if (service == null) throw NullPointerException();
  this.service = service;
}

public void doWork() {
  if ( service != null ) {
    service.doWork();
  }
}

Ответ 3

Вы правы в отношении эффектов volatile, поэтому это должно быть правильно, но я запутался в вашем дизайне. Я не понимаю, почему вы не просто синхронизируете setService - это, вероятно, не будет вызываться часто. Если это называется более чем один раз, "if (serviceReady)" часть является спорной, так как он все равно будет верно, но это нормально, так как замена атомарная, если я правильно понимаю.

Я понимаю, что service.doWork() является потокобезопасным, да?

Ответ 4

В теории он никогда не должен работать. Вы хотите обеспечить согласованность памяти для двух переменных, и вы хотите полагаться на volatile read на первом. Волатильное чтение только гарантирует, что поток чтения видит последнее значение переменной. Так что это определенно не так сильно, как вход в заблокированный (синхронизированный) раздел.

На практике это может работать, в зависимости от реализации волатильности используемой вами JVM. Если волатильные чтения выполняются путем сброса всех кэшей ЦП, он должен работать. Но я готов поспорить, что этого не произойдет. Могу ли я заставить когерентность кэша на многоядерном процессоре x86? хорошо читается в отношении этой темы.

Я бы сказал, что просто имеет общую блокировку (java.util.concurrent.Lock или синхронизирован) для двух переменных и выполняться с ней.


Спецификация языка Java, третье издание, это говорит об изменчивости:

8.3.1.4 летучие поля

Поле может быть объявлено изменчивым, и в этом случае модель памяти Java (§17) гарантирует, что все потоки будут видеть согласованное значение для переменной.

и

17.4.4 Порядок синхронизации

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

В нем ничего не говорится о влиянии видимости других переменных.