Публикация и чтение энергонезависимого поля

public class Factory {
    private Singleton instance;
    public Singleton getInstance() {
        Singleton res = instance;
        if (res == null) {
            synchronized (this) {
                res = instance;
                if (res == null) {
                    res = new Singleton();
                    instance = res;
                }
            }
        }
        return res;
    }
}

Это почти правильная реализация потокобезопасного Singleton. Единственная проблема, я вижу:

Поток thread #1 который инициализирует поле instance может быть опубликован до того, как он будет полностью инициализирован. Теперь второй поток может читать instance в несогласованном состоянии.

Но для моего глаза это только проблема. Это только проблема? (И мы можем сделать instance volatile).

Ответы

Ответ 1

Вы, например, объяснены Shipilev в безопасной публикации и безопасной инициализации в Java. Я настоятельно рекомендую прочитать всю статью, но, чтобы подвести итог, посмотрите раздел UnsafeLocalDCLFactory:

public class UnsafeLocalDCLFactory implements Factory {
  private Singleton instance; // deliberately non-volatile

  @Override
  public Singleton getInstance() {
    Singleton res = instance;
    if (res == null) {
      synchronized (this) {
        res = instance;
        if (res == null) {
           res = new Singleton();
           instance = res;
        }
      }
    }
    return res;
  }
}

Вышеперечисленные проблемы:

Введение локальной переменной здесь - исправление корректности, но только частичное: до сих пор не происходит - до публикации одного экземпляра Singleton и чтения любого из его полей. Мы только защищаем себя от возвращения "null" вместо экземпляра Singleton. Тот же трюк можно также рассматривать как оптимизацию производительности для SafeDCLFactory, т.е. Делать только одно неустойчивое чтение, получая:

Shipilev предлагает исправить следующим образом, пометив instance volatile:

public class SafeLocalDCLFactory implements Factory {
  private volatile Singleton instance;

  @Override
  public Singleton getInstance() {
    Singleton res = instance;
    if (res == null) {
      synchronized (this) {
        res = instance;
        if (res == null) {
          res = new Singleton();
          instance = res;
        }
      }
    }
    return res;
  }
}

Других проблем с этим примером нет.

Ответ 2

Это хороший вопрос, и я постараюсь обобщить мое понимание здесь.

Предположим, что Thread1 в настоящее время инициализирует экземпляр Singleton и публикует ссылку (явно неясно). Thread2 может видеть эту небезопасную опубликованную ссылку (что означает, что она видит Thread2 ссылку), но это не означает, что поля, которые он видит через эту ссылку (поля Singleton, которые инициализируются через конструктор) также инициализируются правильно.

Насколько я вижу, это происходит потому, что может быть переопределение хранилищ полей, происходящих внутри конструктора. Поскольку нет правил "бывает раньше" (это простые переменные), это может быть вполне возможным.

Но это не единственная проблема. Обратите внимание, что вы читаете два чтения:

if (res == null) { // read 1

return res // read 2

Эти считывания не имеют защиты от синхронизации, поэтому они читаются в режиме плавного чтения. AFAIK это означает, что чтение 1 разрешено читать ненулевую ссылку, а чтение 2 разрешено читать нулевую ссылку.

Эта битва - это то же самое, что объясняет ВСЕ могучий Шипилев (даже если я читаю эту статью раз в полгода, я все равно каждый раз нахожу что-то новое).

Действительно, создание volatile востановит ситуацию. Когда вы делаете его неустойчивым, это происходит:

 instance = res; // volatile write, thus [LoadStore][StoreStore] barriers

Все "другие" действия (магазины внутри конструктора) не могут пройти этот забор, повторных заказов не будет. Это также означает, что когда вы читаете переменную volatile и видите ненулевое значение, это означает, что каждая "запись", которая была сделана до написания самого изменчивого, произошла точно. Этот отличный пост имеет точное значение этого

Это также решает вторую проблему, так как эти операции не могут быть переупорядочены, вы гарантированно увидите одно и то же значение от read 1 и read 2.

Независимо от того, насколько я читаю и пытаюсь понять, эти вещи постоянно сложны для меня, очень мало людей, которых я знаю, которые могут написать такой код и правильно спросить об этом. Когда вы сможете (я делаю!), Пожалуйста, придерживайтесь известных и рабочих примеров двойной проверки блокировки :)

Ответ 3

Я делаю это вот так:

public class Factory {
    private static Factory factor;
    public static Factory getInstance() {
        return factor==null ? factor = new Factory() : factor;
    }
}

Просто

Ответ 4

Обычно я больше никогда не буду использовать механизм с двойной проверкой. Чтобы создать потокобезопасный синглтон, вы должны позволить компилятору сделать это:

public class Factory {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return res;
    }
}

Теперь вы говорите, чтобы сделать инстант неустойчивым. Я не думаю, что это необходимо с этим решением, поскольку jit-компилятор теперь обрабатывает синхронизацию потоков при построении объекта. Но если вы хотите сделать его изменчивым, вы можете.

Наконец, я бы сделал getInstance() и экземпляр static. Тогда вы можете ссылаться на Factory.getInstance() напрямую, не создавая класс Factory. Кроме того: вы получите один и тот же экземпляр для всех потоков в приложении. Кроме того, каждый новый Factory() предоставит вам новый экземпляр.

Вы также можете посмотреть Википедию. У них есть чистое решение, если вам нужно ленивое решение:

https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java

// Correct lazy initialization in Java
class Foo {
    private static class HelperHolder {
       public static final Helper helper = new Helper();
    }

    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}

Ответ 5

Теперь второй поток может читать экземпляр в несогласованном состоянии.

Я уверен, что на самом деле это единственная проблема в этом коде. Как я это понимаю, как только линия

instance = res;

другой поток может читать instance и рассматривать его как ненулевое, и, таким образом, пропускает synchronized. Это означает, что между этими двумя потоками не происходит, потому что они существуют только в том случае, если оба потока синхронизируются на одном и том же объекте или получают доступ к тем же самым изменчивым полям.

Другие ответы уже связаны с безопасной публикацией и безопасной инициализацией на Java, которая предлагает следующие способы решения небезопасной публикации:

  • Заставить instance поле volatile. Все потоки должны читать одну и ту же volatile переменную, которая устанавливает связь между событиями

    Происходит запись в нестабильное поле (§8.3.1.4) - перед каждым последующим чтением этого поля.

  • Обертка синглтона в обертку, которая хранит синглтон в final поле. Правила для final полей не так формально указаны, как отношения, происходящие раньше, лучшее объяснение, которое я могу найти, - это окончательная полевая семантика

    Объект считается полностью инициализированным, когда его конструктор заканчивается. Поток, который может видеть только ссылку на объект после того, как этот объект был полностью инициализирован, гарантированно увидит правильно инициализированные значения для конечных полей этого объекта.

    (Не акцент и ограничение на конечные поля, другие поля могут быть замечены в несогласованном состоянии, по крайней мере теоретически)

  • Убедившись, что сам синглтон содержит только конечные поля. Объяснение будет таким же, как и выше.

Ответ 6

Проблема с кодом, упомянутым в вопросе, заключается в том, что может произойти reordering, и поток может получить частично построенный объект одноэлементного класса.

Когда я говорю о reordering, я имею в виду следующее:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
                /* The above line executes the following steps:
                   1) memory allocation for Singleton class
                   2) constructor call ( it may have gone for some I/O like reading property file etc...
                   3) assignment ( look ahead shows it depends only on memory allocation which has already happened in 1st step. 
                   If compiler changes the order, it might assign the memory allocated to the instance variable. 
                   What may happen is half initialized object will be returned to a different thread )
                */
            }
        }
    }
    return instance;
} 

Объявление переменного экземпляра volatile обеспечивает бывает-перед/заказанными отношениями на указанном выше 3 шага:

Происходит запись в нестабильное поле (§8.3.1.4) - перед каждым последующим чтением этого поля.


Из Википедии блокировки с двойной проверкой:

С J2SE 5.0 эта проблема была исправлена. Теперь ключевое слово volatile гарантирует, что несколько потоков обрабатывают экземпляр singleton правильно. Эта новая идиома описана в "Декларации с двойной проверкой блокировки":

// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }

    // other functions and members...
}