На примере Java-кода с многопоточностью покажите проблему двойной проверки (double-checked locking) и предложите корректное решение: class Lazy{ private static Resource r; public static Resource get(){ if(r==null){ synchronized(Lazy.class){ if(r==null) r=new Resource(); } } return r; } } — обсудите volatile, инициализацию и стоимость синхронизации

27 Окт в 13:43
5 +1
0
Ответы
1
Коротко: проблема в том, что при создании объекта 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 для надёжной ленивой инициализации.
27 Окт в 14:45
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир