Почему volatile используется в двойной проверке блокировки

Из книги образцов конструкции Head First реализован шаблон с единым блоком с двойной проверкой блокировки, как показано ниже:

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 не противоречит цели использования двойной проверки блокировки, то есть производительности?

Ответ 1

Хороший ресурс для понимания того, почему требуется volatile, поступает из книги JCIP. Википедия имеет достойное объяснение этого материала.

Реальная проблема заключается в том, что Thread A может назначить ячейку памяти для instance, прежде чем она завершит построение instance. Thread B увидит это назначение и попытается его использовать. Это приводит к ошибке Thread B, поскольку используется частично построенная версия instance.

Ответ 2

Как цитируется @irreputable, volatile не дорогая. Даже если это дорого, согласованность должна быть отдана приоритету над производительностью.

Есть еще один чистый элегантный способ для Lazy Singletons.

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 из wikipedia

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

Поскольку для инициализации класс не имеет переменных static, инициализация завершается тривиально.

Определение статического класса LazyHolder внутри него не инициализируется до тех пор, пока JVM не определит, что LazyHolder должен быть выполнен.

Статический класс LazyHolder выполняется только тогда, когда статический метод getInstance вызывается в классе Singleton, и в первый раз это произойдет, JVM будет загружать и инициализировать класс LazyHolder.

Это решение является потокобезопасным, не требуя специальных языковых конструкций (т.е. volatile или synchronized).

Ответ 3

Хорошо, нет двойной проверки блокировки производительности. Это сломанный шаблон.

Оставляя эмоции в стороне, volatile здесь, потому что без него к тому времени, когда второй поток проходит instance == null, первый поток может еще не построить new Singleton(): no one promises, что происходит создание объекта - перед назначением instance для любого потока, но фактически создающего объект.

volatile в свою очередь устанавливает связь между чтением и записью и исправляет разбитый шаблон.

Если вы ищете производительность, используйте вместо этого внутренний статический класс.

Ответ 4

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

Первая из них не для правильности (если бы вы были правы, что это было бы самозавершением), а скорее для оптимизации.

Ответ 5

Неустойчивое чтение не очень дорого само по себе.

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

Другой способ заключается в использовании поля final (см. wikipedia). Для этого требуется дополнительное чтение, которое может стать дороже, чем версия volatile. Версия final может быть быстрее в узком цикле, однако этот тест является спорным, как ранее утверждалось.

Ответ 6

Объявление переменной volatile гарантирует, что все обращения к ней фактически будут считывать текущее значение из памяти.

Без volatile компилятор может оптимизировать доступ к памяти и сохранить его значение в регистре, поэтому только первое использование переменной считывает фактическую ячейку памяти, содержащую эту переменную. Это проблема, если переменная изменена другим потоком между первым и вторым доступом; первый поток имеет только копию первого (предварительно измененного) значения, поэтому второй оператор if проверяет устаревшую копию значения переменной.