Одна статья, чтобы решить барьер памяти

Java Архитектура JVM переводчик

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

Эта статья предназначена только для того, чтобы помочь понять механизмы параллелизма, предоставляемые JVM. Сначала из семантики volatile вводится проблема видимости и переупорядочивания, далее объясняется принцип проблемы, и объясняется причина необходимости барьеров памяти, затем стандарт барьеров памяти и поддержка производителей для барьеров памяти кратко обсуждаются, и volatile используется в качестве примера Обсудите, как барьеры памяти решают эти проблемы, наконец, добавьте несколько инкапсуляций, сделанных JVM поверх барьеров памяти. Для облегчения понимания некоторые базовые принципы на уровне аппаратной архитектуры (особенно архитектуры ЦП) будут кратко обсуждены, но механизмы реализации не будут подробно рассмотрены.

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

Если есть неточности, поправьте меня!

правила изменяемой переменной

Хорошим примером введения барьеров памяти являетсяvolatile变量规则.

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

определение

правила изменяемой переменной:Запись в изменчивую переменную должна выполняться перед чтением переменной..

Правило volatile-переменной — это просто стандарт, который требует, чтобы реализации JVM гарантировали семантику частичного порядка для volatile-переменных.Объединение правил порядка программ, транзитивность, семантика частичного порядка обычно имеет два эффекта:

  • поддерживать видимость
  • Отключить переупорядочивание (операции чтения запрещают операции после переупорядочивания, операции записи запрещают операции до переупорядочивания)

Пополнить:

  • Правило порядка выполнения программы: если операция A предшествует операции B в программе, то операция A будет выполняться перед операцией B в потоке.
  • Транзитивность: если операция А выполняется до операции В, а операция В выполняется до операции С, то операция А должна выполняться до операции С.

В дальнейшем, если задействована только видимость, указывается «видимость»; если задействованы оба, это называется «частичным порядком». Изменение порядка определенно приводит к проблемам с видимостью, поэтому не существует сценария, в котором изменение порядка обсуждалось бы отдельно.

правильная осанка

Использование правила volatile-переменной несколько раз рассматривалось в предыдущих статьях.

Простая в использовании только гарантия видимости правила volatile для самой volatile переменной:

Комплексное использование правил volatile переменных (в сочетании с правилами порядка программы, транзитивность) гарантирует частичный порядок самой переменной и других окружающих переменных:

Видимость и изменение порядка

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

видимость

определение

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

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

Видимость можно считать самой слабой"一致性"(弱一致), гарантированно непротиворечивы только данные, видимые пользователем, но это не гарантирует, что сохраненные данные непротиворечивы в любое время (强一致). Проблема «видимости кеша» обсуждается ниже, а в некоторых статьях она также упоминается как проблема «когерентности кеша».

источник проблемы

Одна из самых простых проблем с видимостью связана с архитектурой кэша внутри компьютера:

image.png

Кэши значительно сокращают разрыв между быстрыми процессорами и медленной памятью. Возьмем в качестве примера трехуровневую архитектуру кэша:

  • Кэш L1 находится ближе всего к процессору, имеет наименьшую емкость (например, 32K, 64K и т. д.) и самую высокую скорость, на каждое ядро ​​приходится по одному кэшу L1.
  • Кэш L2 имеет большую емкость (например, 256 КБ) и меньшую скорость, как правило, на каждом ядре имеется независимый кэш L2.
  • Кэш L3 ближе всего к памяти, имеет наибольшую емкость (например, 12 МБ) и имеет самую низкую скорость.Ядра в одном сокете ЦП совместно используют кэш L3.

Чтобы быть точным, на каждом ядре есть два кэша L1, один для кэша данных L1d и один для кэша инструкций L1i.

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

  1. Если Core0 и Core1 попадут в один и тот же адрес в памяти, их соответствующие кэши L1 будут кэшировать копию одних и тех же данных.
  2. Изначально и Core0, и Core1 дружно читают эти данные.
  3. Вдруг Core0 выйдет из строя, он модифицирует данные, делая данные в двух кешах разными, точнее данные в Core1 L1 Cache失效.

В одноядерную эру есть только Core0, а Core0 может модифицировать Core0 для чтения, и проблем нет, однако после модификации _Core0 Core1 не знает, что данные просрочены, и продолжает использовать _ сдуру, что может привести к ошибкам расчета данных в легких, а в тяжелых случаях - к бесконечным циклам и программам, сбоям и т. д.

Актуальная проблема видимости распространяется в двух направлениях:

  • Помимо кеша L3,В аппаратной архитектуре также есть множество кэшей, реализованных разными производителями, и все они имеют схожие проблемы с видимостью.. Например, регистр эквивалентен кешу между ЦП и кешем L1.
  • В многопоточных моделях памяти различных языков высокого уровня (включая Java),Самостоятельное поддержание кеша в стеке потоков — обычная оптимизация, но очевидно, что все терпит неудачу из-за проблем с видимостью кеша на уровне ЦП..

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

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

изменение порядка

определение

Переупорядочивание строго не определено. В целом его можно разделить на два типа:

  • Истинный переупорядочивание:Компилятор, базовое оборудование (ЦП и т. д.) для целей «оптимизации»., который переупорядочивает инструкции в соответствии с некоторым правилом (хотя иногда это выглядит не по порядку).
  • Псевдопереупорядочение: из-заПорядок синхронизации кэшад., похоже инструкции переупорядочены.

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

Первый контакт с концепцией переупорядочивания должен сбивать с толку, будьте терпеливы, будьте терпеливы.

источник проблемы

Проблемы с изменением порядка возникают постоянно и возникают по трем сценариям:

  1. Оптимизация компилятора во время компиляции
  2. Неупорядоченная оптимизация при выполнении процессора
  3. Порядок синхронизации кэша (вызывающий проблемы с видимостью)

Сценарии 1 и 2 относятся к истинному переупорядочению, сценарий 3 относится к ложному переупорядочению. Сценарий 3 также является проблемой наглядности, и для согласованности мы сначала обсудим сценарий 3.

Псевдопереупорядочивание из-за видимости

Порядок синхронизации кэша по своей сути является проблемой видимости.

Предположение程序顺序(порядок программы), сначала обновляется переменная v1, а затем обновляется переменная v2, независимо от истинного переупорядочения:

  1. Core0 сначала обновляет v1 в кеше, а затем обновляет v2 в кеше (расположенном в двух строках кеша, чтобы строки кеша не записывались обратно в память вместе при их удалении).
  2. Core0 читает v1 (при условии, что протокол LRU используется для вытеснения кэша).
  3. Кэш Core0 заполнен, и самая дальняя используемая версия v2 записывается обратно в память.
  4. Кэш Core1 изначально хранил v1, а теперь загружает в кеш v2.

Переупорядочивание для порядка программы.Если порядок выполнения инструкции отличается от порядка программы, это означает, что эта инструкция переупорядочивается.

В настоящее время,Core1 видит только последнее значение v2, но не v1, хотя событие «обновление v1» происходит до «обновление v2».. Это из-за видимостиПсевдопереупорядочивание: похоже, что переупорядочение происходит, хотя фактического переупорядочения нет..

Как видите, видимость кеша вызывает не только проблемы с видимостью, но и ложное переупорядочивание. следовательно,Пока проблема видимости в кеше решена, псевдопереупорядочивание решено.

протокол МЭСИ

Вернемся к примеру с проблемой видимости и определением видимости. Решить эту задачу просто, применяя определение видимости, нужно всего лишь:После того, как Core0 изменит данные v, пусть Core1 получит последнее измененное значение v, прежде чем использовать v..

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

  1. После того, как Core0 изменит v, он отправляет сигнал, чтобы пометить v, кэшированный Core1, как недействительный, и записать измененное значение обратно в память.
  2. Core0 может изменять v несколько раз, и для каждой модификации отправляется только один сигнал (шина между кэшами будет заблокирована при отправке сигнала), а v, закэшированный Core1, поддерживает флаг недействительности.
  3. Перед тем, как Core1 использует v, он обнаруживает, что v в кэше признан недействительным, и знает, что v был изменен, поэтому перезагружает v из других кэшей или памяти.

Вышеизложенное является основным принципом протокола MESI (Modified Exclusive Shared Or Invalid, четыре состояния кеша) Он не является строгим, но его достаточно для понимания видимости кеша (более известного как «консистентность кеша»).

Протокол MESI решает проблему видимости на уровне кэша ЦП.

Ниже приведена машина состояний кеша протокола MESI, просто взгляните на нее:

image.png

государство:

  • M (Modified, Modified): Локальный процессор модифицировал строку кэша, то есть грязную строку, ее содержимое не совпадает с содержимым в памяти.И этот кэш имеет только одну локальную копию (эксклюзивную).
  • E (Exclusive, Exclusive): Содержимое строки кэша такое же, как и в памяти, и у других процессоров нет этой строки данных.
  • S (Shared, Shared): Содержимое строки кэша такое же, как и в памяти, и возможно, что другие процессоры также имеют копии этой строки кэша.
  • I (Invalid, Invalid): строка кэша недействительна и не может быть использована.
остаточная проблема

Теперь, когда у нас есть протокол MESI, нет ли необходимости в семантике видимости volatile? Конечно нет, есть еще три проблемы:

  • Не все аппаратные архитектуры обеспечивают одинаковые гарантии согласованности, для JVM требуется изменчивая унифицированная семантика.(Даже МЭСИ решает проблему только на уровне кеша процессора и не задействует другие уровни).
  • Проблемы с видимостью связаны не только с кешем ЦП, но и с моделью памяти, поддерживаемой самой JVM. Использование volatile в качестве маркера может решить проблему видимости на уровне JVM.
  • Без учета истинного переупорядочения MESI решает проблему видимости на уровне кэша ЦП, однако истинное переупорядочение также может вызвать проблемы с видимостью.

Пока первый вопрос называется "内存可见性проблема, которая решается барьерами памяти. Обсуждается позже.

Оптимизация компилятора во время компиляции

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

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

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

К счастью, поскольку это переупорядочение на уровне компилятора, компилятор, естественно, может им управлять. Пометка volatile отключает переупорядочивание на уровне компилятора.

Неупорядоченная оптимизация при выполнении процессора

Неупорядоченная оптимизация на уровне процессора значительно экономит время ожидания и повышает производительность процессора.

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

Без внеочередной оптимизации процесс выполнения инструкций процессора выглядит следующим образом:

  1. получение команды.
  2. Если входной операнд доступен (например, уже существует в регистре), инструкция отправляется соответствующему функциональному блоку. Если один или несколько операндов недоступны в текущем тактовом цикле (обычно их необходимо извлечь из основной памяти), процессор будет ждать, пока они не станут доступны.
  3. Инструкции выполняются в соответствующих функциональных блоках.
  4. Функциональный блок записывает результат операции обратно в регистр.

Процесс выполнения при внеочередной оптимизации выглядит следующим образом:

  1. получение команды.
  2. Инструкции отправляются в виде последовательности инструкций (также называемой执行缓冲区или保留站)середина.
  3. Инструкция будет ждать последовательно, пока ее операнд данных не будет доступен. Затем командам разрешается покидать буфер последовательности перед первой, более старой инструкцией.. (здесь показано не по порядку)
  4. Инструкции назначаются и выполняются соответствующим функциональным блоком.
  5. Результаты выстраиваются в последовательность.
  6. Результат этой инструкции записывается в регистр только после того, как все предыдущие инструкции записали свои результаты в регистр. (переставлены не по порядку результаты)

Конечно, для того, чтобы добиться внеочередной оптимизации, требуется много технической поддержки, такой как寄存器重命名,分枝预测д., но общего понимания этого достаточно. Реализация барьера памяти будет дана соответственно в комментариях ниже.

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

public class OutofOrderExecution {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    
    public static void main(String[] args)
        throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                a = 1;
                x = b;
            }
        });
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                b = 1;
                y = a;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(“(” + x + “,” + y + “)”);
    }
}

Независимо от переупорядочения компилятора и проблем с видимостью кеша, что может вывести приведенный выше код?

Наиболее вероятен результат(0,1),(1,0)или(1,1). Поскольку потоки t1 и t2 могут выполняться последовательно или наоборот, t1 и t2 могут выполняться попеременно.

Тем не мение,Результат выполнения этого кода также может быть (0,0), что кажется нелогичным. Это результат неупорядоченного выполнения процессора: нет никакой зависимости данных между двумя строками кода внутри потока t1, поэтомуx = bвышел из строяa = 1раньше; в то же время в потоке t2y = aраньше, чем в потоке t1a = 1воплощать в жизнь. Возможная последовательность выполнения следующая:

  1. t1: x = b
  2. t2: b = 1
  3. t2: y = a
  4. t1: a = 1

Здесь код эквивалентен инструкции, которая не является строгой, но не мешает пониманию.

Кажется, что проблему, вызванную указанным выше переупорядочением (или нарушением порядка), можно назвать проблемой «видимости». Однако вред от такого переупорядочивания гораздо больше, чем от чистой видимости, потому что не все инструкции являются простыми операциями чтения или записи —Сколько способов написать одноэлементный шаблон в интервью?иРоль и принцип ключевого слова volatileупоминается в部分初始化пример, это不安全发布Это связано с переоформлением. следовательно,Неправильно классифицировать изменение порядка как проблему «видимости», только то, что изменение порядка вызывает проблемы с видимостью..

Тем не менее, простого решения проблемы видимости памяти недостаточно, инужно специальное решение处理器重排序Проблема.

Конечно, некоторые процессоры не переупорядочивают инструкции или могут выполнять их не по порядку из-за зависимости данных между несколькими ядрами. В настоящее время volatile используется только для унификации семантики переупорядочивания.

барьер памяти

Барьер памяти (Memory Barrier) и забор памяти (Memory Fence) — это одно и то же понятие, но разные названия.

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

В настоящее время Monkey не проверял следующий анализ и делает выводы и компромиссы только с учетом логики и дизайна системы. Эксперименты будут добавлены позже.

стандартный

Сначала кратко разберитесь с двумя командами:

  • Сохранить: сбросить данные, кэшированные процессором, в память.
  • Загрузить: Копирует данные, хранящиеся в памяти, в кэш процессора.
Тип барьера Пример инструкции инструкция
LoadLoad Barriers Load1;LoadLoad;Load2 Этот барьер гарантирует, что данные Load1 загружаются до Load2 и всех последующих инструкций загрузки.
StoreStore Barriers Store1;StoreStore;Store2 Этот барьер гарантирует, что немедленная запись данных в память Store1 (что делает их видимыми для других процессоров) предшествует Store2 и всем последующим инструкциям сохранения.
LoadStore Barriers Load1;LoadStore;Store2 Убедитесь, что загрузка данных Load1 предшествует операции сброса данных в память Store2 и всем последующим инструкциям сохранения.
StoreLoad Barriers Store1;StoreLoad;Load2 Этот барьер гарантирует, что немедленная запись данных в память Store1 предшествует Load2 и всем последующим инструкциям загрузки. Он выполнит все инструкции доступа к памяти (инструкции сохранения и инструкции доступа) до завершения барьера перед выполнением инструкций доступа к памяти после барьера.

Барьеры StoreLoad также имеют эффект трех других барьеров, поэтому их также называют全能屏障(mfence), который в настоящее время поддерживается большинством процессоров, но по сравнению с другими барьерами накладные расходы этого барьера относительно высоки.

Тем не мение,В дополнение к mfence, разные архитектуры ЦП реализуют барьеры памяти очень по-разному и в той степени, в которой они реализованы.. Условно говоря, процессоры Intel强内存模型чем у DEC Alpha弱复杂内存模型(кэш не только многоуровневый, но и секционированный) Проще. Архитектура x86 является наиболее распространенной в многопоточном программировании, и реализация барьеров памяти в архитектуре x86 обсуждается ниже.

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

Но каким бы ни был план,Реализация барьеров памяти должна быть рассчитана на неупорядоченное выполнение.. Основные принципы внеочередного исполнения объясняются в предыдущих комментариях: ядро ​​— этобуфер последовательности, пока операнд данных инструкции доступен для выборки, инструкции разрешается покинуть буфер последовательности и начать выполнение до первой, более старой инструкции. Для семантики видимости памяти барьеры памяти могут быть реализованы с использованием идеи, аналогичной протоколу MESI. Обезьяна не стала продолжать изучение механизма реализации семантики переупорядочивания.Осуществимая идея такова:

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

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

Барьеры памяти для архитектуры x86

Архитектура x86 не реализует полные барьеры памяти.

Store Barrier

Директива sfence реализует Store Barrier, который эквивалентен StoreStore Barriers.

Принудительное выполнение всех инструкций сохранения до инструкции sfence до выполнения инструкции sfence, отправка сигнала аннулирования кеша и сброс данных в буфере сохранения в кэш L1 ЦП; все инструкции сохранения после инструкции sfence, Все выполняется после выполнения инструкции sfence. То есть запрещается переупорядочивать инструкции сохранения до и после инструкции sfence для охвата инструкции sfence, чтобыВсе обновления памяти, которые произошли до барьера магазина, видны.

«Видимый» здесь означаетИзмененные значения видны(видимость памяти) иРезультат операции виден(отключить переупорядочивание). То же ниже.

В стандарте барьера памяти обсуждение между кешем и памятью相干性, собственно, то же самое относится и к многоуровневым кэшам между регистрами и кэшами, и даже между регистрами и памятью. Архитектура x86 использует вариант протокола MESI.Протокол обеспечивает корреляцию между трехуровневым кешем и памятью.Барьер памяти нужен только для того, чтобы буфер хранения (который можно рассматривать как слой кеша между регистром и L1 Cache) и L1 Cache. То же ниже.

Load Barrier

Инструкция lfence реализует барьер нагрузки, который эквивалентен барьеру нагрузки.

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

Full Barrier

Инструкция mfence реализует Full Barrier, который эквивалентен StoreLoad Barriers.

Инструкция mfence объединяет функции инструкции sfence и инструкции lfence, заставляя все инструкции сохранения/загрузки, предшествующие инструкции mfence, выполняться до выполнения инструкции mfence; все инструкции сохранения/загрузки после инструкции mfence выполняются в инструкции mfence. после казни. То есть переупорядочивание инструкций сохранения/загрузки до и после инструкции mfence запрещается для перекрытия инструкции mfence, так чтоВсе операции, которые произошли до полного барьера, видны всем операциям, которые происходят после полного барьера.

Как volatile решает проблемы видимости памяти и переупорядочения процессора

На уровне компилятора volatile используется только как маркер для отмены кэширования и переупорядочивания на уровне компилятора.

Если сама аппаратная архитектура имеет гарантированную видимость памяти (например, одноядерный процессор, модель памяти с достаточной согласованностью и т. д.), то volatile является нулевым флагом и не будет вставлять барьер памяти с соответствующей семантикой.

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

Если нет гарантии, то, взяв за пример архитектуру x86, JVM обрабатывает volatile-переменные следующим образом:

  • После записи изменчивой переменной v вставьте sfence. Таким образом, все хранилища до sfence (включая запись v) не будут переупорядочены после sfence, а все хранилища после sfence не будут переупорядочены до sfence, что отключит переупорядочивание хранилища между sfence; а значения, измененные до sfence, будут записывается обратно в кеш и помечает недействительность кеша в других процессорах.
  • Вставьте ограждение перед чтением изменчивой переменной v. Таким образом, загрузка (включая чтение v) после lfence не будет переупорядочена до lfence, а загрузка до lfence не будет переупорядочена после lfence, отключив переупорядочение загрузки через lfence; а после lfence сначала будет обновлен недействительный кэш. чтобы получить последнее измененное значение и сотрудничать с sfence, чтобы обеспечить видимость памяти.

На других платформах JVM использует mfence вместо sfence и lfence для более строгой семантики.

Их комбинация реализует правило изменчивой переменной в отношении Happens-Before.

Другие инкапсуляции, сделанные JVM для барьеров памяти

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

С помощью: контрейлерных.

В JVM это обычно относится к: объединению правил порядка выполнения программы Happens-Before с некоторыми другими правилами порядка (обычно правила блокировки монитора, правила изменяемой переменной), чтобы получить доступ к переменной, которая не защищена блокировкой.

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

Следующее обсуждение по-прежнему основано на архитектуре x86.

конечное ключевое слово

Если поле экземпляра объявлено окончательным, JVM вставляет sfence после инициализации конечной переменной.

Последнее поле класса находится в<clinit>()метод, и его видимость гарантируется процессом загрузки класса JVM.

Инициализация конечных полей находится в<init>()метод завершен. sfence отключает переупорядочивание хранилища до и после sfence и обеспечивает видимость обновлений памяти до окончательной инициализации поля (include).

Снова поговорим о частичной инициализации

Вышеупомянутые хорошие свойства называются "初始化安全性". Это гарантирует,Для правильно построенного объекта все потоки видят правильные значения, установленные конструктором для каждого конечного поля объекта, независимо от того, как был опубликован объект.

Это сужает видимость с «обновление памяти перед окончательной инициализацией поля (включить)» до «конечной инициализации поля».. Monkey точную причину не нашел, а проверять что на руках только один jdk не удобно. это может быть потому,JVM не требует, чтобы реализация виртуальной машины генерировала<init>()Последовательность инструкций по инициализации полей при использовании метода.

Безопасность инициализации предлагает новый способ решения проблемы частичной инициализации: если все поля публикуемого объекта являются окончательными, это может предотвратить изменение порядка начальной ссылки на объект до завершения процесса построения. тогда,Сколько способов написать одноэлементный шаблон в интервью?Полноценный вариант three in также может отбрасывать volatile и вместо этого использовать семантику final sfence:

// 饱汉
// ThreadSafe
public class Singleton1_3 {
  private static Singleton1_3 singleton = null;
  
  public int f1 = 1;   // 触发部分初始化问题
  public int f2 = 2;

  private Singleton1_3() {
  }

  public static Singleton1_3 getInstance() {
    if (singleton == null) {
      synchronized (Singleton1_3.class) {
        // must be a complete instance
        if (singleton == null) {
          singleton = new Singleton1_3();
        }
      }
    }
    return singleton;
  }
}

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

CAS

На архитектуре x86 CAS переводится как "lock cmpxchg...".cmpxchg — это инструкция по сборке для CAS. Опора на сигнал блокировки в архитектуре ЦП гарантирует видимость и запрещает изменение порядка.

Префикс блокировки — это специальный сигнал, который выполняется следующим образом:

  • Заблокируйте шину и кэш.
  • Принудительно выполняет все инструкции перед сигналом блокировки и синхронизирует связанные буферы.
  • Выполнить инструкцию после блокировки (например, cmpxchg).
  • Снимите блокировки на шине и кэше.
  • Заставляет все инструкции, следующие за сигналом блокировки, выполняться после этого и синхронизировать соответствующий кэш.

Поэтому сигнал блокировки, хотя и не является барьером памяти, имеет семантику mfence (разумеется, и排他性семантика).

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

Замок

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

Семантика блокировок mfence реализует правила блокировки монитора в отношении Happens-Before.

CAS имеет ту же семантику mfence и должен иметь то же отношение частичного порядка, что и блокировки. Хотя JVM явно этого не требует.


Ссылаться на:


Ссылка на эту статью:Одна статья, чтобы решить барьер памятиавтор:обезьяна 007
Источник:monkeysayhi.github.io
Эта статья основана наCreative Commons Attribution-ShareAlike 4.0Выпущено по международному лицензионному соглашению, приветствуется перепечатка, вывод или использование в коммерческих целях, но авторство и ссылка на эту статью должны быть сохранены.