Летучие семантики по отношению к другим полям
Предположим, что у меня есть следующий код
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) - перед каждым последующим чтением этого поля.
В нем ничего не говорится о влиянии видимости других переменных.