Почему volatile используется в этом примере блокировки с двойной проверкой
Из книги Head First design patterns, синглетный шаблон с двойной проверкой блокировки был реализован следующим образом:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Я не понимаю, почему volatile
используется. Разве использование volatile
не нарушает цель использования двойной проверенной блокировки, т. е. производительности?
6 ответов:
Хороший источник для понимания того, почему
Реальная проблема заключается в том, чтоvolatile
необходим, взят из книги JCIP. Википедия также имеетдостойное объяснение этого материала.Thread A
может назначить пространство памяти дляinstance
до того, как он закончит построениеinstance
.Thread B
увидит это назначение и попытается его использовать. Это приводит к сбоюThread B
, потому что он использует частично сконструированную версиюinstance
.
Как цитирует @irreputable, volatile не стоит дорого. Даже если это дорого, последовательность должна иметь приоритет над производительностью.
Есть еще один чистый элегантный способ для ленивых Синглетов.public final class Singleton { private Singleton() {} public static Singleton getInstance() { return LazyHolder.INSTANCE; } private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } }
Исходная статья: Initialization-on-demand_holder_idiom из Википедии
Поскольку класс не имеет переменныхВ программной инженерии идиома держателя инициализации по требованию (шаблон проектирования)-это синглтон с ленивой загрузкой. Во всех версиях Java эта идиома обеспечивает безопасный, очень одновременная ленивая инициализация с хорошей производительностью
static
для инициализации, инициализация завершается тривиально.Статическое определение класса
LazyHolder
в нем не инициализируется до тех пор, пока JVM не определит, что LazyHolder должен быть выполнен.Статический класс
Это решение потокобезопасно, не требуя специальных языковых конструкций (например,LazyHolder
выполняется только тогда, когда статический методgetInstance
вызывается на синглтоне класса, и в первый раз, когда это происходит, JVM загружается и инициализируется классLazyHolder
.volatile
илиsynchronized
).
Ну, нет никакой перепроверенной блокировки для производительности. Это сломанный шаблон.
Оставляя эмоции в стороне,
volatile
здесь потому, что без него к моменту прохождения второго потокаinstance == null
первый поток может еще не построитьnew Singleton()
: никто не обещает, что создание объектапроизойдет-до назначенияinstance
для любого потока, кроме того, который фактически создает объект.
volatile
в свою очередь устанавливает бывает-до связь между чтениями и записями, и исправляет нарушенный шаблон.Если вы ищете производительность, используйте внутренний статический класс holder.
Если у вас его нет, второй поток может попасть в синхронизированный блок после того, как первый установит его в null, и ваш локальный кэш все равно будет думать, что это null.
Первый - не для корректности (если бы Вы были правы, то это было бы саморазрушительно), а для оптимизации.
Волатильное чтение само по себе не очень дорого.
Вы можете сконструировать тест для вызоваgetInstance()
в узком цикле, чтобы наблюдать влияние изменчивого чтения; однако этот тест не реалистичен; в такой ситуации программист обычно вызываетgetInstance()
один раз и кэширует экземпляр на время использования. Другой impl-это использование поляfinal
(см. Википедию). Это требует дополнительного чтения, которое может стать дороже, чем версияvolatile
. Версияfinal
может быть быстрее в узком круге, однако, этот тест спорен, как утверждалось ранее.
Объявление переменной как
volatile
гарантирует, что все обращения к ней фактически считывают ее текущее значение из памяти.Без
volatile
компилятор может оптимизировать доступ к памяти и сохранить ее значение в регистре, поэтому только первое использование переменной считывает фактическое местоположение памяти, содержащее переменную. Это проблема, если переменная изменяется другим потоком между первым и вторым доступом; первый поток имеет только копию первого (предварительно измененного) значения, так что второй операторif
проверяет устаревшую копию значения переменной.