Java дважды проверил блокировку - PullRequest
38 голосов
/ 26 октября 2009

Я случайно натолкнулся на статью, в которой недавно обсуждался шаблон блокировки с двойной проверкой в ​​Java и его подводные камни, и теперь мне интересно, не подвержен ли какой-либо вариант этого шаблона, который я использую в течение многих лет,

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

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

public class Test {
    private static Test instance;
    private static boolean initialized = false;

    public static Test getInstance() {
        if (!initialized) {
            synchronized (Test.class) {
                if (!initialized) {
                    instance = new Test();
                    initialized = true;
                }
            }
        }
        return instance;
    }
}

Ответы [ 11 ]

25 голосов
/ 26 октября 2009

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

РЕДАКТИРОВАТЬ: Чтобы уточнить ответ выше, оригинальный вопрос, заданный об использовании логического значения для контроля блокировки двойной проверки. Без решений по ссылке выше, это не будет работать. Вы могли бы дважды проверить блокировку, фактически устанавливая логическое значение, но у вас все еще есть проблемы с переупорядочением команд, когда дело доходит до создания экземпляра класса. Предлагаемое решение не работает, потому что экземпляр не может быть инициализирован после того, как вы видите инициализированное логическое значение как истинное в несинхронизированном блоке.

Правильное решение для двойной проверки блокировки - либо использовать volatile (в поле экземпляра) и забыть об инициализированном логическом значении, и обязательно использовать JDK 1.5 или выше, либо инициализировать его в конечном поле, как это было разработано в связанной статье и ответ Тома, или просто не используйте его.

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

16 голосов
/ 26 октября 2009

Это сработало бы, если бы initialized было volatile. Как и в случае synchronized, интересные эффекты volatile на самом деле не столько связаны со ссылкой, сколько мы можем сказать о других данных. Установка поля instance и объекта Test заставляет произойти до записи в initialized. При использовании кэшированного значения через короткое замыкание initialize чтение происходит до чтения instance и объектов, достигших через ссылку. Нет особой разницы в наличии отдельного флага initialized (кроме того, что это вызывает еще большую сложность в коде).

(Правила для полей final в конструкторах для небезопасной публикации немного отличаются.)

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

Код слишком сложный. Вы могли бы просто написать это как:

private static final Test instance = new Test();

public static Test getInstance() {
    return instance;
}
12 голосов
/ 26 октября 2009

Блокировка с двойной проверкой действительно сломана, и решение проблемы на самом деле проще реализовать по кодам, чем эта идиома - просто используйте статический инициализатор.

public class Test {
    private static final Test instance = createInstance();

    private static Test createInstance() {
        // construction logic goes here...
        return new Test();
    }

    public static Test getInstance() {
        return instance;
    }
}

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

5 голосов
/ 19 августа 2010

Это причина, по которой блокировка с двойной проверкой нарушена.

Синхронизация гарантирует, что только один поток может ввести блок кода. Но это не гарантирует, что изменения переменных, выполненные в синхронизированном разделе, будут видны другим потокам. Только потоки, которые входят в синхронизированный блок, гарантированно видят изменения. Это причина, почему блокировка с двойной проверкой нарушена - она ​​не синхронизируется на стороне читателя. Поток чтения может увидеть, что синглтон не равен нулю, но данные синглтона могут быть не полностью инициализированы (видимы).

Заказ предоставляется volatile. volatile гарантирует порядок, например, запись в энергозависимое одноэлементное статическое поле гарантирует, что запись в одноэлементный объект будет завершена до записи в энергозависимое статическое поле. Это не мешает создавать синглтон из двух объектов, это обеспечивается синхронизацией.

Конечные статические поля класса не должны быть переменными. В Java эта проблема решается JVM .

См. Мой пост, ответ на Шаблон синглтона и сломанная блокировка с двойной проверкой в ​​реальном Java-приложении , иллюстрирующий пример синглтона в отношении блокировки с двойной проверкой это выглядит умно, но сломано.

0 голосов
/ 19 мая 2017

Во-первых, для синглетов вы можете использовать Enum, как описано в этом вопросе Реализация Singleton с Enum (на Java)

Во-вторых, начиная с Java 1.5, вы можете использовать энергозависимую переменную с двойной проверкой блокировки, как описано в конце этой статьи: https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

0 голосов
/ 06 марта 2016

Блокировка с двойной проверкой - это анти-шаблон.

Класс Lazy Initialization Holder - это шаблон, на который вы должны смотреть.

Несмотря на так много других ответов, я решил, что должен ответить, потому что до сих пор нет одного простого ответа, который говорит, почему DCL нарушается во многих контекстах, почему он не нужен и что вы должны вместо этого делать. Поэтому я буду использовать цитату из Goetz: Java Concurrency In Practice, которая для меня дает наиболее краткое объяснение в ее последней главе о модели памяти Java.

Речь идет о безопасной публикации переменных:

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

Последующие изменения в JMM (Java 5.0 и более поздние версии) позволили DCL работать, если ресурс стал энергозависимым, и влияние на производительность этого невелико, поскольку энергозависимые чтения обычно лишь немного дороже, чем энергонезависимые чтения.

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

Листинг 16.6. Ленивый держатель инициализации Класс идиома.

public class ResourceFactory
    private static class ResourceHolder {
        public static Resource resource = new Resource();
    }

    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}

Это способ сделать это.

0 голосов
/ 07 октября 2015

Проблема DCL не работает, хотя кажется, что она работает на многих виртуальных машинах. Здесь есть хорошее описание проблемы http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html.

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

Единственный способ правильно решить эту проблему - это избежать ленивой инициализации (сделать это с нетерпением) или выполнить одиночную проверку внутри синхронизированного блока. Использование логического initialized эквивалентно нулевой проверке самой ссылки. Второй поток может видеть initialized как истинное, но instance может все еще быть нулевым или частично инициализированным.

0 голосов
/ 22 августа 2015

Я изучал идиому блокировки с двойной проверкой и, насколько я понял, ваш код может привести к проблеме чтения частично созданного экземпляра, ЕСЛИ ваш класс Test не является неизменным:

Модель памяти Java предлагает специальную гарантию безопасности инициализации для совместного использования неизменяемых объектов.

К ним можно безопасно обращаться, даже если синхронизация не используется для публикации ссылки на объект.

(Цитаты из очень рекомендуемой книги Java Concurrency in Practice)

Так что в этом случае идиома блокировки с двойной проверкой будет работать.

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

Булева переменная ничего не добавляет, чтобы избежать проблемы, потому что она может быть установлена ​​в true до инициализации класса Test (ключевое слово synchronized не предотвращает переупорядочение полностью, некоторые выражения могут изменить порядок). В модели памяти Java нет правила «до того, как это произойдет».

И создание логической переменной volatile также ничего не добавит, потому что 32-битные переменные создаются атомарно в Java. Идиома блокировки с двойной проверкой будет работать и с ними.

Начиная с Java 5, вы можете исправить эту проблему, объявив переменную экземпляра как volatile.

Подробнее об идиоме с двойной проверкой вы можете прочитать в этой очень интересной статье .

Наконец, я прочитал несколько рекомендаций:

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

  • Тщательно продумайте, действительно ли необходима оптимизация блокировки с двойной проверкой перед ее реализацией, потому что в большинстве случаев это не стоило бы усилий. Кроме того, рассмотрите возможность создания класса Test в статическом поле, поскольку отложенная загрузка полезна только тогда, когда создание класса занимает много ресурсов, и в большинстве случаев это не так.

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

0 голосов
/ 23 апреля 2013

В некоторых случаях возможна двойная проверка.

  1. Во-первых, если вам действительно не нужен синглтон, и двойная проверка используется только для НЕ создания и инициализации многих объектов.
  2. В конце конструктора / инициализированного блока установлено поле final (которое приводит к тому, что все ранее инициализированные поля будут видны другим потокам).
0 голосов
/ 17 октября 2012

Если «initialized» имеет значение true, тогда «instance» ДОЛЖЕН быть полностью инициализирован, так же как 1 плюс 1 равно 2 :). Поэтому код правильный. Экземпляр создается только один раз, но функция может вызываться миллион раз, поэтому она повышает производительность без проверки синхронизации на миллион минус один раз.

...