Дан Java-код с паттерном двойной-роверности (double-checked locking): public Singleton getInstance() { if (instance == null) { synchronized(this) { if (instance == null) instance = new Singleton(); } } return instance; } Почему это может быть небезопасно в Java без дополнительных мер, и как правильно реализовать ленивую и потокобезопасную инициализацию
Коротко: исходный код небезопасен потому, что без дополнительных гарантий может произойти переупорядочивание при создании объекта: поток А может выполнить выделение памяти и присвоение ссылки полю `instance` до завершения инициализации объекта; тогда поток Б увидит ненулевую ссылку и получит частично сконструированный объект. Это поведение допускалось моделью памяти Java до и даже без явных гарантий видимости. Правильный способ — гарантировать видимость и порядок через механизмы JMM. Почему так происходит (кратко): - Конструктор может выполняться не полностью до того, как ссылка будет записана в `instance` (реализация `new` может быть разбита на шаги: allocate → assign → initialize). - Без барьеров памяти другой поток может увидеть ненулевую ссылку и работать с неинициализированным объектом. - Начиная с Java 5+5+5+ модель памяти фиксирована, но требуется использовать соответствующие средства (например, `volatile`) для предотвращения переупорядочивания. Правильные варианты реализации (коротко, с примерами): 1) Исправление двойной проверки через `volatile` (работает в Java 5+5+5+): ```java public class Singleton { private static volatile Singleton instance; // volatile обязателен 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 (лениво, потокобезопасно, без синхронизации при доступе): ```java public class Singleton { private Singleton() {} private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } } ``` 3) Enum-синглтон (самый простой и безопасный — сериализация/рефлексия учтены): ```java public enum Singleton { INSTANCE; // методы... } ``` 4) Простой synchronized-метод (корректно, но синхронизация при каждом вызове): ```java public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; } } ``` Вывод: либо используйте `volatile` вместе с двойной проверкой (при условии Java 5+5+5+), либо предпочтительнее — Holder-idiom или enum для простоты и безопасности.
Почему так происходит (кратко):
- Конструктор может выполняться не полностью до того, как ссылка будет записана в `instance` (реализация `new` может быть разбита на шаги: allocate → assign → initialize).
- Без барьеров памяти другой поток может увидеть ненулевую ссылку и работать с неинициализированным объектом.
- Начиная с Java 5+5+5+ модель памяти фиксирована, но требуется использовать соответствующие средства (например, `volatile`) для предотвращения переупорядочивания.
Правильные варианты реализации (коротко, с примерами):
1) Исправление двойной проверки через `volatile` (работает в Java 5+5+5+):
```java
public class Singleton {
private static volatile Singleton instance; // volatile обязателен
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 (лениво, потокобезопасно, без синхронизации при доступе):
```java
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
```
3) Enum-синглтон (самый простой и безопасный — сериализация/рефлексия учтены):
```java
public enum Singleton {
INSTANCE;
// методы...
}
```
4) Простой synchronized-метод (корректно, но синхронизация при каждом вызове):
```java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) instance = new Singleton();
return instance;
}
}
```
Вывод: либо используйте `volatile` вместе с двойной проверкой (при условии Java 5+5+5+), либо предпочтительнее — Holder-idiom или enum для простоты и безопасности.