Разберите проблему с двойной проверкой (double-checked locking) в Java и почему без volatile она может быть небезопасна; приведите безопасную альтернативу
Коротко и по существу. Проблема - Двойная проверка (double-checked locking, DCL) в классическом виде: ``` class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } ``` Необходима для уменьшения стоимости синхронизации (синхронизируемся только при инициализации). - Почему это небезопасно без volatile: При создании объекта JVM/JIT/процессор могут выполнять операции в другом порядке. Создание объекта по сути может быть представлено как последовательность: 111 выделить память, 222 выполнить конструктор (инициализировать поля), 333 записать ссылку в переменную `instance`. Релабелинг/перестановка может привести к порядку 111, 333, 222: ссылка записана, но объект ещё не полностью инициализирован. Тогда второй поток увидит `instance != null` и начнёт использовать частично сконструированный объект — это race condition и неисправимое поведение по спецификации Java Memory Model до и без использования специальных средств синхронизации. - Роль `volatile`: Объявление `private static volatile Singleton instance;` предотвращает такие перестановки и гарантирует видимость: записи, сделанные в конструкторе до записи `instance`, будут видимы всем последующим чтениям `instance`. В модели памяти Java (с фиксированными семантиками начиная с Java 555) запись в volatile обладает «release» семантикой, чтение — «acquire»; это обеспечивает корректность DCL. Безопасные альтернативы (рекомендуемые) 1) DCL с volatile (если нужен ленивый init и низкая накладная после инициализации): ``` class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } ``` 2) Initialization-on-demand holder (лучше по читаемости и безопасности, лениво и без volatile): ``` class Singleton { private Singleton() {} private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } } ``` 3) Enum singleton (самый простой и безопасный способ для одиночки): ``` public enum Singleton { INSTANCE; // методы } ``` 4) Простая синхронизированная фабрика (правильно, но может быть дороже при частых вызовах): ``` class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; } } ``` Вывод - DCL без `volatile` небезопасен из‑за возможной перестановки записи ссылки и инициализации объекта; начиная с Java 555 правильное решение — либо использовать `volatile`, либо предпочтительнее Holder/enum idiom.
Проблема
- Двойная проверка (double-checked locking, DCL) в классическом виде:
```
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
```
Необходима для уменьшения стоимости синхронизации (синхронизируемся только при инициализации).
- Почему это небезопасно без volatile:
При создании объекта JVM/JIT/процессор могут выполнять операции в другом порядке. Создание объекта по сути может быть представлено как последовательность:
111 выделить память, 222 выполнить конструктор (инициализировать поля), 333 записать ссылку в переменную `instance`.
Релабелинг/перестановка может привести к порядку 111, 333, 222: ссылка записана, но объект ещё не полностью инициализирован. Тогда второй поток увидит `instance != null` и начнёт использовать частично сконструированный объект — это race condition и неисправимое поведение по спецификации Java Memory Model до и без использования специальных средств синхронизации.
- Роль `volatile`:
Объявление `private static volatile Singleton instance;` предотвращает такие перестановки и гарантирует видимость: записи, сделанные в конструкторе до записи `instance`, будут видимы всем последующим чтениям `instance`. В модели памяти Java (с фиксированными семантиками начиная с Java 555) запись в volatile обладает «release» семантикой, чтение — «acquire»; это обеспечивает корректность DCL.
Безопасные альтернативы (рекомендуемые)
1) DCL с volatile (если нужен ленивый init и низкая накладная после инициализации):
```
class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
```
2) Initialization-on-demand holder (лучше по читаемости и безопасности, лениво и без volatile):
```
class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
```
3) Enum singleton (самый простой и безопасный способ для одиночки):
```
public enum Singleton {
INSTANCE;
// методы
}
```
4) Простая синхронизированная фабрика (правильно, но может быть дороже при частых вызовах):
```
class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) instance = new Singleton();
return instance;
}
}
```
Вывод
- DCL без `volatile` небезопасен из‑за возможной перестановки записи ссылки и инициализации объекта; начиная с Java 555 правильное решение — либо использовать `volatile`, либо предпочтительнее Holder/enum idiom.