"Эффективная Java" головоломка: почему в этом параллельном коде требуется волатильность?
Я работаю по пункту 71, "Использовать ленивую инициализацию разумно", "Эффективная Java" (второе издание). Это предполагает использование идиомы двойной проверки для ленивой инициализации полей экземпляра с помощью этого кода (стр. 283):
private volatile FieldType field;
FieldType getField() {
FieldType result = field;
if (result == null) { //First check (no locking)
synchronized(this) {
result = field;
if (result == null) //Second check (with locking)
field = result = computeFieldValue();
}
}
return result;
}
Итак, у меня есть несколько вопросов:
-
Зачем нужен изменчивый модификатор на field
, если инициализация происходит в синхронизированном блоке? Книга предлагает этот вспомогательный текст: "Поскольку нет блокировки, если поле уже инициализировано, важно, чтобы поле было объявлено изменчивым". Следовательно, так ли это, что после инициализации поля volatile является единственной гарантией множественных согласованных потоков на field
, учитывая отсутствие другой синхронизации? Если да, почему бы не синхронизировать getField(), или это так, что приведенный выше код обеспечивает лучшую производительность?
-
В тексте предлагается, чтобы не требуемая локальная переменная result
использовалась для "обеспечения того, чтобы field
читается только один раз в обычном случае, когда он уже инициализирован", тем самым повышая производительность. Если result
был удален, как бы field
было прочитано несколько раз в обычном случае, когда он уже был инициализирован?
Ответы
Ответ 1
Почему необходим изменчивый модификатор в поле, если инициализация происходит в синхронизированном блоке?
volatile
необходимо из-за возможного переупорядочения инструкций вокруг построения объектов. Модель памяти Java заявляет, что компилятор реального времени имеет возможность изменять порядок инструкций для перемещения инициализации поля вне конструктора объекта.
Это означает, что thread-1 может инициализировать field
внутри synchronized
, но этот поток-2 может видеть, что объект не полностью инициализирован. Любые незавершенные поля не должны быть инициализированы до того, как объект был назначен field
. Ключевое слово volatile
гарантирует, что field
будет полностью инициализирован до его доступа.
Это пример знаменитой ошибка двойной проверки блокировки.
Если результат был удален, как поле будет читаться несколько раз в обычном случае, когда он уже был инициализирован?
В любое время, когда вы обращаетесь к полю volatile
, это приводит к пересечению барьера памяти. Это может быть дорого по сравнению с доступом к нормальному полю. Копирование поля volatile
в локальную переменную является общим шаблоном, если к нему нужно обращаться несколько раз одним и тем же методом.
См. мой ответ здесь для получения дополнительных примеров опасностей совместного использования объекта без барьеров памяти между потоками:
Ссылка на объект перед конструктором объекта завершена
Ответ 2
Это довольно сложно, но теперь это связано с тем, что компилятор может изменить ситуацию.
В принципе шаблон Double Checked Locking
не работает в Java, если только переменная volatile
.
Это связано с тем, что в некоторых случаях компилятор может назначить переменную так что-то отличное от нуля, а затем инициализировать переменную и переназначить ее. В другом потоке будет видно, что переменная не равна нулю и пытается ее прочитать - это может вызвать всевозможные особые результаты.
Посмотрите this другой вопрос SO по этой теме.
Ответ 3
Хорошие вопросы.
Почему необходим изменчивый модификатор в поле, если инициализация происходит в синхронизированном блоке?
Если у вас нет синхронизации и вы назначаете это глобальное глобальное поле, не будет никаких обещаний, что все записи, которые происходят при построении этого объекта, будут видны. Например, представьте, что FieldType выглядит.
public class FieldType{
Object obj = new Object();
Object obj2 = new Object();
public Object getObject(){return obj;}
public Object getObject2(){return obj2;}
}
Возможно, что getField()
возвращает непустой экземпляр, но методы экземпляра getObj()
и getObj2()
могут возвращать нулевые значения. Это связано с тем, что без синхронизации записи в эти поля могут участвовать в гонке с целью создания объекта.
Как это фиксируется с изменчивым? Все записи, которые происходят до волатильной записи, видны после этой волатильной записи.
Если результат был удален, как поле будет читаться несколько раз в обычном случае, когда он уже был инициализирован?
Сохранение локально один раз и чтение по всему методу обеспечивает одно локальное хранилище потоков/процессов и все локальные чтения потоков. Вы можете спорить о преждевременной оптимизации в этом отношении, но мне нравится этот стиль, потому что вы не будете запускать себя в странные проблемы переупорядочения, которые могут возникнуть, если вы этого не сделаете.