Чому летючі властивості використовуються в подвійно перевіреному блокуванні


77

З книги "Шаблони дизайну 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перешкоджає використання цілі використання подвійно перевіреного блокування, тобто продуктивності?


6
Я думав, подвійна перевірка блокування зламана, чи хтось це виправив?
Девід Хеффернан,

4
Для чого це варте, я знайшов шаблони дизайну Head First - це жахлива книга, на якій можна вчитися. Коли я оглядаюсь на нього, тепер цілком логічно, що я дізнався про зразки в іншому місці, але вчитися, не знаючи зразків, насправді не слугувало його цілі. Але це дуже популярно, тож, можливо, це був просто я, коли я щільний. :-)
corsiKa

@DavidHeffernan Я бачив, що цей приклад використовується як єдиний спосіб, яким можна довірити jvm робити DCL.
Натан Фегер,

FWIW, в системі x86 мінливе читання-читання повинно призвести до відмови. Насправді, єдина операція, яка вимагає обмеження для постійності пам’яті, - це нестабільне запис-читання. Отже, якщо ви дійсно пишете значення лише один раз, тоді вплив повинен бути мінімальним. Я не бачив, щоб хтось насправді оцінював це, і думаю, результат був би цікавим!
Тім Бендер

@DavidHeffernan з усіх практичних причин він все ще не працює. Краще з volatile(як, наприклад, "не викрутить вашу програму"), але тоді ви насправді не сильно виграєте.
alf,

Відповіді:


70

Хороший ресурс для розуміння того, для чого volatileце потрібно, походить із книги JCIP . У Вікіпедії також є гідне пояснення цього матеріалу.

Справжня проблема полягає в тому, що він Thread Aможе призначити простір у пам'яті до того, instanceяк його закінчити будувати instance. Thread Bпобачить це призначення і спробує використати його. Це призводить до Thread Bпомилки, оскільки використовується частково побудована версія instance.


1
Добре, схоже, нова реалізація енергонезалежних виправила проблеми з пам’яттю з DCL. Що я все ще не отримую, це наслідки продуктивності використання летючого тут. З того, що я прочитав, volatile майже настільки ж повільний, як і синхронізований, так чому б не просто синхронізувати весь виклик методу getInstance ()?
toc777

5
@ toc777 volatileповільніше, ніж звичайний файл. Якщо ви шукаєте продуктивності, виберіть модель власника. volatileтут просто для того, щоб показати, що існує спосіб змусити зламаний шаблон працювати. Це швидше завдання кодування, ніж реальна проблема.
alf,

6
@ Тим часом, синглтон у XML все одно синглтон; розуміння стану виконання програми не стає простішим за допомогою DI. менші одиниці коду можуть здатися простішими, за рахунок сильної структуризації всіх одиниць відповідно до ідіоми DI (добре, що деякі можуть сказати). Звинувачення на адресу синглтона не є справедливим, воно плутає API з impl - Foo.getInstance()це просто вираз, щоб якось отримати Foo, він нічим не відрізняється від @Inject Foo foo; в будь-якому випадку сайт, який запитує Foo, є агностичним щодо того, який Foo повертається, і як, в будь-якому випадку статична залежність і залежності від середовища виконання однакові.
беззаперечний

2
@irreputable Ви знаєте, що смішно, що на той час, коли у нас був цей обмін, я ніколи не користувався Spring і не мав на увазі хиткий DI Spring. Справжньою небезпекою для Singleton як a static factoryє спокуса назвати його глибоко всередині коду, який не повинен знати про getInstance()метод і замість цього вимагати надання екземпляра.
Тім Бендер

1
The real problem is that Thread A may assign a memory space for instance before it is finished constructing instance. То як volatile вирішити це?
shaoyihe

21

Як цитує @irreputable, нестабільне не є дорогим. Навіть якщо це дорого, послідовності слід надавати пріоритет над ефективністю.

Є ще один елегантний елегантний спосіб для Ледачих Самотніх.

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).


11

Ну, немає подвійної перевірки блокування для продуктивності. Це зламаний візерунок.

Залишаючи емоції осторонь, volatileце тут, тому що без нього до моменту проходження другого потоку instance == nullперший потік ще може не побудувати new Singleton(): ніхто не обіцяє, що створення об’єкта відбувається до призначення instanceбудь-якому потоку, крім того, який фактично створює об’єкт.

volatileв свою чергу встановлює, що було раніше зв'язок " між читанням і записом і виправляє порушений шаблон.

Якщо ви шукаєте продуктивність, використовуйте натомість внутрішній статичний клас власника.


1
hi @ alf , Згідно з визначенням подій-до, після того, як один потік звільняє замок, інший потік отримує замок, тоді останній може бачити попередню зміну. Якщо так, я не думаю, що мінливе ключове слово потрібно. Чи можете ви пояснити це більш детально
Цинь Донг Лян

Але у випадку, якщо другий потік потрапляє лише на зовнішній, якщо блокування блокування не відбувається, тож замовлення немає.
alf

Привіт @ alf, отже, ти намагаєшся стверджувати, що коли перший потік створює екземпляр всередині синхронізованого блоку, тоді цей екземпляр все ще може бути нульовим для другого потоку, оскільки кеш пропускає, і він інсталює його знову, якщо екземпляр не мінливий? Ви можете пояснити?
Aarish Ramesh

1
@AarishRamesh, не null; будь-якої держави є дві операції: присвоєння адреси instanceзмінній і фактичне створення об’єкта за цією адресою. Якщо немає чогось, що примушує синхронізацію, наприклад, volatileдоступ або явну синхронізацію, другий потік може вивести ці дві події з ладу.
alf

2

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

Перший - не для коректності (якщо б ви були правильним, що це було б самовпевненим), а для оптимізації.


1
Згідно з визначенням "до-до", після того, як один потік звільняє замок, інший потік отримує замок, тоді останній може бачити попередню зміну. Якщо так, я не думаю, що мінливе ключове слово потрібно. Чи можете ви пояснити це більш докладно
Цинь Донг Лян

@QinDongLiang Якщо змінна не є мінливою, другий потік може використовувати кешоване значення у власному стеку. Нестабільність змушує його повернутися до джерела, щоб отримати належне значення. Звичайно, він повинен робити це при кожному доступі, і, отже, може мати хіт продуктивності (але, будьмо чесними, якщо це не надто критичний цикл, це, мабуть, не найгірше у вашій системі ...)
corsiKa

2

Оголошення змінної як volatile гарантує, що всі звернення до неї фактично зчитують її поточне значення з пам'яті.

Без volatileцього компілятор може оптимізувати доступ до змінної до пам'яті (наприклад, збереження її значення в реєстрі), тому лише перше використання змінної зчитує фактичне місце пам’яті, що містить змінну. Це проблема, якщо змінна змінена іншим потоком між першим та другим доступом; перший потік має лише копію першого (попередньо зміненого) значення, тому другий ifоператор перевіряє застарілу копію значення змінної.


4
-1 Я сьогодні втрачаю свої репутаційні бали :) Справжня причина полягає в тому, що є кеш пам'яті, змодельований як локальна пам'ять потоку. Порядок, в якому локальна пам’ять змивається з основною, не визначений, тобто якщо у вас немає траплялося раніше відносин, наприклад, за допомогою volatile. Реєстри не мають нічого спільного з неповними об'єктами та проблемою DCL.
alf

3
Ваше визначення volatileзанадто вузьке - якби це було все нестабільне, подвійна перевірка блокування спрацювала б чудово в <Java5.volatileвводить бар'єр пам'яті, що робить певне переупорядкування незаконним - без цього, навіть якщо ми ніколи не читаємо застарілі значення з пам'яті, це все одно буде небезпечно. Редагувати: alf побив мене, не повинен був приготувати собі гарного чаю;)
Voo

1
@TimBender, якщо синглтон містить мінливий стан, змивання це не має нічого спільного з посиланням на сам синглтон (ну, є непряме посилання, оскільки доступ до volatlieпосилання на синглтон змушує ваш потік перечитати основну пам'ять - але це вторинний ефект , не причина проблеми :))
alf

@alf, ти маєш рацію. І насправді зробити екземпляр volatile не допомагає, якщо стан всередині є змінним, оскільки змив відбувається лише за умови зміни самого посилання (як, наприклад, створення масивів / списків volatile не робить нічого для вмісту). Крейда це до мозку пердеть.
Тім Бендер

Згідно з визначенням "до-до", після того, як один потік звільняє замок, інший потік отримує замок, тоді останній може бачити попередню зміну. Якщо так, я не думаю, що мінливе ключове слово потрібно. Чи можете ви пояснити це детальніше @Tim Bender
Qin Dong Liang

1

Нестабільне читання саме по собі насправді не є дорогим.

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

Інший імпл - використання final поля (див. Wikipedia). Для цього потрібно додаткове читання, яке може стати дорожчим за volatileверсію. finalВерсія може бути швидше в тугий петлі, однак , що тест є спірним , як стверджував раніше.


0

Подвійна перевірка блокування - це техніка, яка запобігає створенню чергового екземпляра одиночного під час виклику getInstance методу здійснюється у багатопотоковому середовищі.

Звернути увагу

  • Екземпляр Singleton перевіряється двічі перед ініціалізацією.
  • Синхронізований критичний розділ використовується лише після першої перевірки одиничного екземпляра з цієї причини для підвищення продуктивності.
  • volatileключове слово в оголошенні члена екземпляра. Це дозволить компілятору завжди читати з основної пам'яті та писати в неї, а не в кеш-пам'яті процесора. З volatileгарантованою змінною взаємозв'язок "відбувається до", все записування відбуватиметься перед будь-яким зчитуванням змінної екземпляра.

Недоліки

  • Оскільки це вимагає volatile ключове слово належної роботи, воно не сумісне з Java 1.4 та старішими версіями. Проблема полягає в тому, що невпорядкована запис може дозволити повернення посилання на екземпляр до того, як буде виконаний конструктор singleton.
  • Проблема продуктивності через зменшення кешу для летючої змінної.
  • Екземпляр Singleton перевіряється двічі перед ініціалізацією.
  • Це досить багатослівно, і це ускладнює читання коду.

Існує кілька варіантів реалізації одиночного шаблону, кожен із яких має переваги та недоліки.

  • Прагнення до завантаження синглтона
  • Перевірена блокування одинарної
  • Ідіома власника ініціалізації на вимогу
  • Синглтон на основі переліку

Детальний опис кожного з них занадто багатослівний, тому я просто розмістив посилання на хорошу статтю - Все, що ви хочете знати про Сінглтон

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.