Почему для режима блокировки с двойной проверкой требуется volatile ?

Java

Шаблон блокировки двойной проверки часто встречается в исходном коде некоторых фреймворков для ленивой инициализации переменных. Этот шаблон также можно использовать для создания синглетонов. Давайте посмотрим на пример блокировки с двойной проверкой в ​​​​Spring.

DCL.png

В этом примере файл конфигурации необходимо загрузить вhandlerMappings, потому что чтение ресурсов занимает много времени, поэтому поместите действие в реальную потребностьhandlerMappingsкогда. Мы видим, чтоhandlerMappingsиспользовался раньшеvolatile. Вы когда-нибудь задумывались, зачем это нужноvolatile? Хотя я и раньше понимал принцип режима блокировки с двойной проверкой, я игнорировал использование переменных.volatile.

Давайте посмотрим на причины этого.

Плохой пример ленивой инициализации

Говоря о ленивой инициализации переменной, самый простой пример — взять переменную для суждения.

errorexample.png

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

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

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

DCLerror.png

Этот метод сначала определяет, инициализирована переменная или нет, а затем получает блокировку. После получения блокировки снова проверьте, инициализирована ли переменная. Цель второго суждения состоит в том, что, возможно, другие потоки получили блокировку и инициализировали сумму изменения. Вторая проверка не прошла до фактической инициализации переменной.

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

Эта схема сужает область применения замка, снижает накладные расходы на замок и выглядит идеально. Однако в этой схеме есть некоторые проблемы, которые легко упустить из виду.

Директива нового экземпляра

Упускаемая из виду проблема заключается в том, чтоCache cache=new Cache()Эта строка кода не является атомарной инструкцией. использоватьjavap -cИнструкция для быстрого просмотра байт-кода.

	// 创建 Cache 对象实例,分配内存
       0: new           #5                  // class com/query/Cache
       // 复制栈顶地址,并再将其压入栈顶
       3: dup
	// 调用构造器方法,初始化 Cache 对象
       4: invokespecial #6                  // Method "<init>":()V
	// 存入局部方法变量表
       7: astore_1

Из байт-кода видно, что создание экземпляра объекта можно разделить на три шага:

  1. Выделить объектную память
  2. Вызов метода конструктора для выполнения инициализации
  3. Присвоить ссылку на объект переменной.

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

Спецификация языка Java указывает, что потоки выполняют программы, которым нужно следовать.intra-thread semantics. **внутрипоточная семантика** гарантирует, что переупорядочивание не изменит результат выполнения программы в одном потоке. Такое переупорядочение может повысить производительность выполнения программы без изменения результата выполнения однопоточной программы.

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

image.png

В приведенном выше примере кода ошибки блокировки с двойной проверкой, если поток 1 получает блокировку и вступает в процесс создания экземпляра объекта, в это время происходит переупорядочение инструкций. Когда поток 1 выполняется до t3, просто входит поток 2. Поскольку в это время объект больше не является нулевым, поток 2 может свободно обращаться к объекту. Тогда объект не был инициализирован, поэтому при доступе к нему потока 2 возникнет исключение.

летучий эффект

Необходимо использовать правильный шаблон блокировки с двойной проверкой.volatile.volatileВ основном он содержит две функции.

  1. Гарантированная видимость. использоватьvolatileОпределенная переменная гарантирует видимость для всех потоков.
  2. Отключите оптимизацию переупорядочения инструкций.

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

Уведомление,volatileОтключение переупорядочивания инструкций не было исправлено до выхода JDK 5.

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

Пересмотрите дважды проверенный код блокировки в Spring.

DCL.png

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

Использование локальных переменных может повысить производительность по сравнению с тем, чтобы не использовать локальные переменные. В основном из-заvolatileСоздание объекта переменных требует отключения переупорядочивания инструкций, что требует дополнительной работы.

Суммировать

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

справочная документация

Блокировка с двойной проверкой и отложенная инициализация
Примечания к теме «Недействительность блокировки с двойной проверкой»

其他平台.png