Почему блокировка с двойной проверкой нарушена на Java? - программирование
Подтвердить что ты не робот

Почему блокировка с двойной проверкой нарушена на Java?

Этот вопрос относится к поведению старых версий Java и старых реализаций алгоритма двойной проверки блокировки

Более новые реализации используют volatile и полагаются на слегка измененную семантику volatile, поэтому они не нарушаются.


Было указано, что присваивание полей всегда атомарно, за исключением полей long или double.

Но когда я прочитал объяснение, почему блокировка двойной проверки нарушена, он сказал, что проблема в операции присваивания:

// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }

    // other functions and members...
}
  1. Поток A замечает, что значение не инициализировано, поэтому он получает блокировку и начинает инициализировать значение.
  2. Из-за семантики некоторых языков программирования код, сгенерированный компилятором, может обновлять совместно используемую переменную, чтобы указывать на частично сконструированный объект, прежде чем A завершит выполнение инициализации.
  3. Поток B замечает, что совместно используемая переменная была инициализирована (или она так выглядит), и возвращает ее значение. Поскольку поток B считает, что значение уже инициализировано, он не получает блокировку. Если B использует объект до того, как вся инициализация, выполненная A, будет видна B (либо потому, что A еще не завершила его инициализацию, либо потому, что некоторые из инициализированных значений в объекте еще не перколированы в используемую память B (когерентность кэша)), программа, скорее всего, потерпит крах.
    http://en.wikipedia.org/wiki/Double-checked_locking).

Когда это возможно? Возможно ли, что на 64-битной JVM операция присваивания не является атомарной? Если нет, то действительно ли нарушена "двойная проверка блокировки"?

4b9b3361

Ответ 1

Проблема заключается не в атомарности, а в упорядочении. JVM разрешено изменять порядок инструкций для повышения производительности, если происходит до не нарушается. Следовательно, среда выполнения может теоретически планировать инструкцию, которая обновляет helper, прежде чем все инструкции из конструктора класса helper выполняются.

Ответ 2

Назначение ссылки является атомарным, но конструкция не является! Итак, как указано в объяснении, предположим, что поток B хочет использовать синглтон до того, как Thread A полностью его построил, он не может создать новый экземпляр, поскольку ссылка не является нулевой, поэтому он просто возвращает частично сконструированный объект.

Если вы не гарантируете, что публикация общая ссылка происходит до другой поток загружает ссылки, то запись ссылка на новый объект может быть переупорядочивается с помощью записи в его поля. В этом случае другой поток может видеть актуальное значение для ссылка на объект, но устаревшая значения для некоторых или всех объектов состояние - частично построенное объект. - Брайан Гетц: Java Concurrency на практике

Так как начальная проверка для null не синхронизирована, публикация отсутствует, и это переупорядочение возможно.

Ответ 3

Для построения экземпляра Helper внутри конструктора может потребоваться несколько назначений, и семантика позволяет их переупорядочить относительно присваивания helper = new Helper().

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

Ответ 5

Прочтите эту статью: http://www.javaworld.com/jw-02-2001/jw-0209-double.html Даже если вы не понимаете всех деталей (например, я), просто верьте, что этот хороший трюк не работает.

Ответ 6

Мне жаль, что это может быть немного неуместно в вопросе, мне просто интересно. В этом случае лучше ли было бы получить блокировку перед назначением и/или вернуть значение? Как:

private Lock mLock = new ReentrantLock();
private Helper mHelper = null;

private Helper getHelper() {
    mLock.lock();
    try {
        if (mHelper == null) {
            mHelper = new Helper();
        }
        return mHelper;
    }
    finally {
        mLock.unlock();
    }
}

Или есть ли преимущество использования блокировки с двойной проверкой?

Ответ 7

/*Then the following should work.
  Remember: getHelper() is usually called many times, it is BAD 
  to call synchronized() every time for such a trivial thing!
*/
class Foo {

private Helper helper = null;
private Boolean isHelperInstantiated;
public Helper getHelper() {
    if (!isHelperInstantiated) {
        synchronized(this) {
            if (helper == null) {
                helper = new Helper();
                isHelperInstantiated = true;
            }
        }
    }
    return helper;
}

// other functions and members...
}