Почему в Java нарушена двойная проверка блокировки? - PullRequest
15 голосов
/ 08 февраля 2011

Этот вопрос относится к поведению старых версий 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 не атомарная? Если нет, то ли «двойная проверка блокировки»"действительно сломан?

Ответы [ 7 ]

15 голосов
/ 08 февраля 2011

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

7 голосов
/ 08 февраля 2011

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

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

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

2 голосов
/ 08 февраля 2011

Двойная проверка блокировки в Java имеет множество проблем:

http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

2 голосов
/ 08 февраля 2011

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

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

1 голос
/ 08 февраля 2011

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

0 голосов
/ 26 сентября 2012
/*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...
}    
0 голосов
/ 07 мая 2012

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

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();
    }
}

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

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