Это безопасная версия блокировки с двойной проверкой? - PullRequest
1 голос
/ 24 февраля 2009

Слегка измененная версия канонической сломанной блокировки с двойной проверкой из Википедии:

class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {

                    // Create new Helper instance and store reference on
                    // stack so other threads can't see it.
                    Helper myHelper = new Helper();

                    // Atomically publish this instance.
                    atomicSet(helper, myHelper);
                }
            }
        }
        return helper;
    }
}

Делает ли публикация только что созданного экземпляра Helper атомарным, делает ли этот дважды проверенный идиом блокировки безопасным, предполагая, что базовая библиотека атомарных операций работает правильно? Я понимаю, что в Java можно было бы просто использовать volatile, но хотя пример приведен в псевдо-Java, предполагается, что это не зависит от языка.

Смотри также:

Двойной проверенный замок Артикул

Ответы [ 6 ]

14 голосов
/ 24 февраля 2009

Это полностью зависит от точной модели памяти вашей платформы / языка.

Мое эмпирическое правило: просто не делайте этого . Программирование без блокировок (или с уменьшенной блокировкой, в данном случае) - это жесткий , и его не следует пытаться делать, если вы не многопоточный ниндзя. Вы должны даже созерцать это , когда у вас есть профилирующее доказательство того, что вам это действительно нужно, и в этом случае вы получите самую лучшую и самую свежую книгу о потоках для этой конкретной платформы и посмотрим, может ли это вам помочь.

7 голосов
/ 24 февраля 2009

Я не думаю, что вы можете ответить на вопрос без учета языка, не отходя от кода полностью. Все зависит от того, как synchronized и atomicSet работают в вашем псевдокоде.

2 голосов
/ 24 февраля 2009

Ответ зависит от языка - он сводится к гарантиям, предоставленным atomicSet().

Если конструкция myHelper может распространяться после atomicSet(), тогда не имеет значения, как переменная назначается общему состоянию.

т.е.

// Create new Helper instance and store reference on
// stack so other threads can't see it.
Helper myHelper = new Helper(); // ALLOCATE MEMORY HERE BUT DON'T INITIALISE

// Atomically publish this instance.
atomicSet(helper, myHelper); // ATOMICALLY POINT UNINITIALISED MEMORY from helper

// other thread gets run at this time and tries to use helper object 

// AT THE PROGRAMS LEISURE INITIALISE Helper object.

Если это разрешено языком, то двойная проверка не будет работать.

0 голосов
/ 26 февраля 2009

Всем, кто переживает за частично построенный объект:

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

Я думаю, что вы путаете частичную конструкцию с другой проблемой того, как компилятор оптимизирует записи. Компилятор может выбрать A) выделить память для нового объекта Helper, B) записать адрес в myHelper (локальная переменная стека), а затем C) вызвать инициализацию любого конструктора. В любое время после точки B и до точки C доступ к myHelper будет проблемой.

Именно эта оптимизация компиляции записей, а не частичная конструкция, связана с цитируемыми статьями. В исходном решении блокировки с одной проверкой оптимизированные записи могут позволить нескольким потокам видеть переменную-член между точками B и C. Эта реализация позволяет избежать проблемы оптимизации записи с помощью локальной переменной стека.

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

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

Исходя из цитируемой статьи, на Java вряд ли будет соответствовать всем этим требованиям. Кроме того, кое-что, что должно быть очень ясно из бумаги, - то, что модель памяти Java часто изменяется. Он адаптируется к лучшему пониманию процессов кэширования, сбора мусора и т. Д., А также адаптируется к изменениям в базовой архитектуре реального процессора, на которой работает виртуальная машина.

Как правило, если вы оптимизируете свой Java-код способом, который зависит от базовой реализации, в отличие от API, вы рискуете получить неработающий код в следующей версии JVM. (Хотя иногда у тебя не будет выбора.)


dsimcha:

Если ваш метод atomicSet является реальным, то я постараюсь отправить ваш вопрос Дугу Ли (вместе с вашей реализацией atomicSet). У меня такое чувство, что он из тех парней, которые ответят. Я предполагаю, что для Java он скажет вам, что дешевле всегда синхронизироваться и искать оптимизацию в другом месте.

0 голосов
/ 24 февраля 2009

Скорее всего, он сломан, потому что проблема частично построенного объекта не решена.

0 голосов
/ 24 февраля 2009

Использование volatile не предотвратит множественные экземпляры, однако использование синхронизации предотвратит создание нескольких экземпляров. Однако, с вашим кодом, возможно, что помощник возвращается до того, как он был настроен (поток 'A' создает его, но до того, как это установится, поток 'B' приходит, помощник не равен нулю и поэтому сразу возвращает его. Исправить эту проблему, удалите первый if (helper == null).

...