it-swarm.com.ru

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

59
toc777

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

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

58
Tim Bender

По словам @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).

14
Ravindra babu

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

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

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

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

7
alf

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

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

1
corsiKa

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

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

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

1
irreputable

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

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

0
David R Tribble