Почему volatile используется в двойной проверке блокировки - PullRequest
64 голосов
/ 22 октября 2011

Из Head First книга шаблонов проектирования, шаблон Singleton с двойной проверкой блокировки реализован следующим образом:

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 ]

58 голосов
/ 22 октября 2011

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

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

16 голосов
/ 19 марта 2016

Как пишет @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 из Википедии

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

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

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

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

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

8 голосов
/ 22 октября 2011

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

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

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

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

2 голосов
/ 22 октября 2011

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

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

1 голос
/ 22 октября 2011

Изменчивое чтение не очень дорого само по себе.

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

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

0 голосов
/ 22 октября 2011

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

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

...