Коротко: проблема в том, что при создании объекта JVM/CPU может выполнить операции в другом порядке (реординг), поэтому другая нитка может увидеть ненулевую ссылку на объект, который ещё не полностью инициализирован. Покажу пример проблемы, затем два корректных решения (с `volatile` и с holder-идиомой) и кратко обсужу `volatile`, инициализацию и стоимость синхронизации. Почему это ломается (суть): создание `new Resource()` условно состоит из шагов (1) выделение памяти,(2) вызов конструктора/инициализация,(3) запись ссылки в переменную r. (1)\ \text{выделение памяти},\qquad (2)\ \text{вызов конструктора/инициализация},\qquad (3)\ \text{запись ссылки в переменную }r. (1)выделениепамяти,(2)вызовконструктора/инициализация,(3)записьссылкивпеременнуюr.
Из-за реординга может случиться порядок (1)→(3)→(2)(1)\to(3)\to(2)(1)→(3)→(2). Тогда другая нитка видит r≠null\,r\neq nullr=null, но объект ещё не инициализирован. Пример, демонстрирующий проблему (встречается редко, но воспроизводимо на сильной конкуренции): ```java class Resource { final int value; Resource() { // эмулируем долгую инициализацию try { Thread.sleep(50); } catch (InterruptedException e) {} value = 42; } } class Lazy { private static Resource r; // <-- без volatile public static Resource get() { if (r == null) { synchronized (Lazy.class) { if (r == null) r = new Resource(); } } return r; } } // Тест: public class Main { public static void main(String[] args) throws Exception { Runnable task = () -> { for (int i = 0; i < 10000; i++) { Resource res = Lazy.get(); if (res.value != 42) { // может оказаться 0 при проблеме System.out.println("BROKEN: value=" + res.value); System.exit(0); } } }; Thread t1 = new Thread(task), t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("OK"); } } ``` Без `volatile` на слабых реализациях/старых JVM можно встретить `value==0` (объект ещё не проинициализирован). Корректное решение 1 — `volatile` (подходит с Java 555 и новее): ```java class Lazy { private static volatile Resource r; // volatile предотвращает реординг и гарантирует видимость public static Resource get() { if (r == null) { synchronized (Lazy.class) { if (r == null) r = new Resource(); } } return r; } } ``` Объяснение: `volatile` запрещает реординг записи поля ссылки относительно инициализации объекта и обеспечивает видимость изменений между нитями (направлено решением JSR-133 с Java 555). Корректное решение 2 — Initialization-on-demand holder (рекомендуется как простое и безопасное): ```java class Lazy { private static class Holder { static final Resource INSTANCE = new Resource(); } public static Resource get() { return Holder.INSTANCE; // безопасно, лениво и без синхронизации } } ``` Преимущество: гарантия ленивой инициализации без явной синхронизации; безопасно по спецификации Java. Ещё вариант для singletons — enum: ```java enum Singleton { INSTANCE; private final Resource resource = new Resource(); public Resource get() { return resource; } } ``` Кратко о `volatile`, инициализации и стоимости синхронизации: - `volatile` обеспечивает упорядочение и видимость: запись в `volatile` не может быть переупорядочена с последующими действиями так, чтобы другая нитка увидела ссылку на неинициализированный объект. - Исправление DCL через `volatile` корректно работает с Java 555+ благодаря новой модель памяти (JSR-133). - Стоимость: современные JVM сильно оптимизировали `synchronized` — нет больших накладных расходов в неконтенстных случаях. Тем не менее: - первый конкурентный вход в критическую секцию может быть дороже (включая эвент/контекст-свитч), контенстность дорого обходится. - `volatile` чтение обычно дешевле, чем уход в синхронизацию, но дороже чистого чтения поля (из-за барьеров памяти). - Практика: предпочитайте holder-идиому или enum для простоты и безопасности. Используйте DCL+`volatile`, если хотите избегать даже небольшой накладной синхронизации после инициализации и уверены в требованиях. Вывод: ваш изначальный код некорректен без `volatile`; добавьте `private static volatile Resource r;` или лучше примените holder-идиому/enum для надёжной ленивой инициализации.
Почему это ломается (суть): создание `new Resource()` условно состоит из шагов
(1) выделение памяти,(2) вызов конструктора/инициализация,(3) запись ссылки в переменную r. (1)\ \text{выделение памяти},\qquad (2)\ \text{вызов конструктора/инициализация},\qquad (3)\ \text{запись ссылки в переменную }r. (1) выделение памяти,(2) вызов конструктора/инициализация,(3) запись ссылки в переменную r. Из-за реординга может случиться порядок (1)→(3)→(2)(1)\to(3)\to(2)(1)→(3)→(2). Тогда другая нитка видит r≠null\,r\neq nullr=null, но объект ещё не инициализирован.
Пример, демонстрирующий проблему (встречается редко, но воспроизводимо на сильной конкуренции):
```java
class Resource {
final int value;
Resource() {
// эмулируем долгую инициализацию
try { Thread.sleep(50); } catch (InterruptedException e) {}
value = 42;
}
}
class Lazy {
private static Resource r; // <-- без volatile
public static Resource get() {
if (r == null) {
synchronized (Lazy.class) {
if (r == null) r = new Resource();
}
}
return r;
}
}
// Тест:
public class Main {
public static void main(String[] args) throws Exception {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
Resource res = Lazy.get();
if (res.value != 42) { // может оказаться 0 при проблеме
System.out.println("BROKEN: value=" + res.value);
System.exit(0);
}
}
};
Thread t1 = new Thread(task), t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("OK");
}
}
```
Без `volatile` на слабых реализациях/старых JVM можно встретить `value==0` (объект ещё не проинициализирован).
Корректное решение 1 — `volatile` (подходит с Java 555 и новее):
```java
class Lazy {
private static volatile Resource r; // volatile предотвращает реординг и гарантирует видимость
public static Resource get() {
if (r == null) {
synchronized (Lazy.class) {
if (r == null) r = new Resource();
}
}
return r;
}
}
```
Объяснение: `volatile` запрещает реординг записи поля ссылки относительно инициализации объекта и обеспечивает видимость изменений между нитями (направлено решением JSR-133 с Java 555).
Корректное решение 2 — Initialization-on-demand holder (рекомендуется как простое и безопасное):
```java
class Lazy {
private static class Holder {
static final Resource INSTANCE = new Resource();
}
public static Resource get() {
return Holder.INSTANCE; // безопасно, лениво и без синхронизации
}
}
```
Преимущество: гарантия ленивой инициализации без явной синхронизации; безопасно по спецификации Java.
Ещё вариант для singletons — enum:
```java
enum Singleton {
INSTANCE;
private final Resource resource = new Resource();
public Resource get() { return resource; }
}
```
Кратко о `volatile`, инициализации и стоимости синхронизации:
- `volatile` обеспечивает упорядочение и видимость: запись в `volatile` не может быть переупорядочена с последующими действиями так, чтобы другая нитка увидела ссылку на неинициализированный объект.
- Исправление DCL через `volatile` корректно работает с Java 555+ благодаря новой модель памяти (JSR-133).
- Стоимость: современные JVM сильно оптимизировали `synchronized` — нет больших накладных расходов в неконтенстных случаях. Тем не менее:
- первый конкурентный вход в критическую секцию может быть дороже (включая эвент/контекст-свитч), контенстность дорого обходится.
- `volatile` чтение обычно дешевле, чем уход в синхронизацию, но дороже чистого чтения поля (из-за барьеров памяти).
- Практика: предпочитайте holder-идиому или enum для простоты и безопасности. Используйте DCL+`volatile`, если хотите избегать даже небольшой накладной синхронизации после инициализации и уверены в требованиях.
Вывод: ваш изначальный код некорректен без `volatile`; добавьте `private static volatile Resource r;` или лучше примените holder-идиому/enum для надёжной ленивой инициализации.