Почему эта двойная проверка блокировки реализована в отдельном классе-обертке? - PullRequest
10 голосов
/ 24 января 2012

Когда я читал статью в Википедии о Двойной проверке блокировки * идиома 1002 *, я запутался в ее реализации:

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) { 
        this.value = value; 
    }
} 
public class Foo {
    private FinalWrapper<Helper> helperWrapper = null;

    public Helper getHelper() {
        FinalWrapper<Helper> wrapper = helperWrapper;

        if (wrapper == null) {
            synchronized(this) {
                if (helperWrapper == null) {
                    helperWrapper = new FinalWrapper<Helper>(new Helper());
                }
                wrapper = helperWrapper;
            }
        }
        return wrapper.value;
    }
}

Я просто не понимаю, зачем нам создавать оболочку,Разве этого недостаточно?

if (helperWrapper == null) {
    synchronized(this) {
        if (helperWrapper == null) {
            helperWrapper = new FinalWrapper<Helper>(new Helper());
        }
    }
}    

Это потому, что использование оболочки может ускорить инициализацию, поскольку оболочка хранится в стеке, а helperWrapper хранится в куче?

Ответы [ 3 ]

2 голосов
/ 05 декабря 2016

Простое использование helperWrapper как для нулевых проверок, так и для оператора return может привести к сбою из-за переупорядочения чтения, разрешенного в модели памяти Java.

Вот пример сценария:

  1. Первый тест helperWrapper == null (racy read) оценивается как false, т. Е. HelperWrapper не равен нулю.
  2. Последняя строка, return helperWrapper.value (racy read) приводит к NullPointerException, т.е. helperWrapper имеет значение null

Как это произошло?Модель памяти Java позволяет переупорядочивать эти два редких чтения, потому что до чтения не было никакого барьера, то есть отношения «происходит до».(См. Пример String.hashCode )

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

0 голосов
/ 24 января 2012

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

Если wrapper равно нулю, то это еще не является неизменнымобъект, и мы должны попасть в блок synchronized.

Если wrapper не равен нулю, то мы гарантируем полностью построенный объект value, поэтому мыможет вернуть его.

В вашем коде проверка null может быть выполнена без фактического чтения ссылочного объекта и, следовательно, без инициирования атомарности операции.Т.е. операция могла бы инициализировать helperWrapper при подготовке к передаче результата new Helper() в конструктор, но конструктор еще не был вызван.

Теперь я предполагаю, что последующий код в вашем примере будетчтение return helperWrapper.value;, которое должно инициировать атомарное чтение ссылки, гарантируя, что конструктор завершает работу, но вполне возможно (семантика некоторых языков программирования '), что компилятору разрешено оптимизироватьчто не выполнить атомарное чтение, и, таким образом, он вернет не полностью инициализированный value объект при точно правильных обстоятельствах.

Выполнение барьера с использованием локальной переменной и эталонной копии заставляет чтение и запись бытьatomic, и гарантирует, что код является поточно-ориентированным.

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

0 голосов
/ 24 января 2012

Разве этого недостаточно?

if (helperWrapper == null) {
     synchronized(this) {
       if (helperWrapper == null) {
          helperWrapper = new FinalWrapper<Helper>(new Helper());
       }
     }
}

Нет, этого недостаточно.

Выше, первая проверка для helperWrapper == null не является поточно-ориентированной.Он может возвратить false (видя ненулевой экземпляр) для некоторого потока «слишком рано», указывая на не полностью созданный объект helperWrapper.

Сама статья Википедии , на которую вы ссылаетесь, объясняет эту проблемупошагово:

Например, рассмотрим следующую последовательность событий:

  1. Поток A замечает, что значение не инициализировано, поэтому он получает блокировку иначинает инициализировать значение.
  2. Из-за семантики некоторых языков программирования код, сгенерированный компилятором, может обновлять совместно используемую переменную, чтобы указывать на частично созданный объект, прежде чем А завершит выполнение инициализации.
  3. Поток B замечает, что общая переменная была инициализирована (или она так выглядит), и возвращает ее значение.Поскольку поток B считает, что значение уже инициализировано, он не получает блокировку.Если B использует объект до того, как вся инициализация, выполненная A, будет видна B (либо потому, что A еще не завершила его инициализацию, либо потому, что некоторые из инициализированных значений в объекте еще не перколированы в используемую память B (когерентность кэша))программа, скорее всего, вылетит.

Примечание * Семантика некоторых языков программирования , упомянутая выше, в точности соответствует семантике Java версии 1.5 и выше.Модель памяти Java (JSR-133) явно допускает такое поведение - ищите в Интернете более подробную информацию об этом, если вам интересно.

Это потому, что использование оболочки может ускорить инициализацию, поскольку оболочка хранитсяв стеке, а helperWrapper хранится в куче?

Нет, выше не причина.

Причина в безопасности потоков.Опять же, семантика Java 1.5 и выше (как определено в модели памяти Java) гарантирует, что любой поток сможет получить доступ только к правильно инициализированному экземпляру Helper из оболочки из-за того, что это конечное поле, инициализированное в конструкторе - см. JLS 17.5 Окончательная семантика поля .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...