Дан фрагмент многопоточного Java-кода с double-checked locking без volatile — объясните, почему это может быть небезопасно, и предложите корректное решение
Почему небезопасно (кратко) - При создании объекта JVM/процессор могут выполнить перестановку операций: (i) выделение памяти, (ii) присвоение ссылки переменной, (iii) инициализация полей. Из-за перестановки ссылка может оказаться видимой другим потокам до завершения инициализации. Тогда поток B увидит непустой экземпляр и начнёт использовать частично сконструированный объект. - Без `volatile` нет гарантии видимости и порядка записи/чтения между потоками по модели памяти Java — следовательно double-checked locking может вернуть «полуготовый» объект. - Эта проблема реально решена только в модели памяти Java после Java 5\text{Java }5Java 5; до этого поведение было особенно ненадёжным. Пример неправильного кода (упрощённо) private static Singleton instance; public static Singleton getInstance() { if (instance == null) { // первый (вне synchronized) чек synchronized (Singleton.class) { if (instance == null) { // второй чек instance = new Singleton(); } } } return instance; } Почему это ломается (схема перестановки) 1.1.1. выделить память для объекта 2.2.2. присвоить ссылку в instance (может выполниться раньше) 3.3.3. выполнить конструктор (инициализацию полей) Если шаг 222 выполнится раньше шага 333, другой поток увидит ненулевую ссылку, но поля ещё не инициализированы. Корректные решения (коротко) 1) Пометить поле как volatile (самый прямой фикс) private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } Пояснение: `volatile` запрещает небезопасные перестановки и обеспечивает видимость; двойная проверка корректна на JVM начиная с Java 5\text{Java }5Java 5. 2) Инициализация через holder-класс (рекомендовано — лениво, потокобезопасно без синхронизации при каждом вызове) private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } Пояснение: загрузка класса `Holder` инициализирует `INSTANCE` потокобезопасно по спецификации JVM. 3) Использовать enum (если это singleton) public enum Singleton { INSTANCE; // методы и поля } Пояснение: простое, безопасное и устойчивое к сериализации решение. 4) Альтернативы: synchronized метод (просто, но синхронизирует каждый вызов) или атомарные структуры (`AtomicReference` + CAS) для специализированных случаев. Короткий вывод - Double-checked locking без `volatile` небезопасен из‑за возможной перестановки и отсутствия гарантий видимости. - Лучшее простое исправление — сделать поле `volatile` или использовать holder-idiom/enum, в зависимости от требований.
- При создании объекта JVM/процессор могут выполнить перестановку операций: (i) выделение памяти, (ii) присвоение ссылки переменной, (iii) инициализация полей. Из-за перестановки ссылка может оказаться видимой другим потокам до завершения инициализации. Тогда поток B увидит непустой экземпляр и начнёт использовать частично сконструированный объект.
- Без `volatile` нет гарантии видимости и порядка записи/чтения между потоками по модели памяти Java — следовательно double-checked locking может вернуть «полуготовый» объект.
- Эта проблема реально решена только в модели памяти Java после Java 5\text{Java }5Java 5; до этого поведение было особенно ненадёжным.
Пример неправильного кода (упрощённо)
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // первый (вне synchronized) чек
synchronized (Singleton.class) {
if (instance == null) { // второй чек
instance = new Singleton();
}
}
}
return instance;
}
Почему это ломается (схема перестановки)
1.1.1. выделить память для объекта
2.2.2. присвоить ссылку в instance (может выполниться раньше)
3.3.3. выполнить конструктор (инициализацию полей)
Если шаг 222 выполнится раньше шага 333, другой поток увидит ненулевую ссылку, но поля ещё не инициализированы.
Корректные решения (коротко)
1) Пометить поле как volatile (самый прямой фикс)
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
Пояснение: `volatile` запрещает небезопасные перестановки и обеспечивает видимость; двойная проверка корректна на JVM начиная с Java 5\text{Java }5Java 5.
2) Инициализация через holder-класс (рекомендовано — лениво, потокобезопасно без синхронизации при каждом вызове)
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
Пояснение: загрузка класса `Holder` инициализирует `INSTANCE` потокобезопасно по спецификации JVM.
3) Использовать enum (если это singleton)
public enum Singleton {
INSTANCE;
// методы и поля
}
Пояснение: простое, безопасное и устойчивое к сериализации решение.
4) Альтернативы: synchronized метод (просто, но синхронизирует каждый вызов) или атомарные структуры (`AtomicReference` + CAS) для специализированных случаев.
Короткий вывод
- Double-checked locking без `volatile` небезопасен из‑за возможной перестановки и отсутствия гарантий видимости.
- Лучшее простое исправление — сделать поле `volatile` или использовать holder-idiom/enum, в зависимости от требований.