Введение
Модель памяти Java — это абстракция более высокого уровня модели аппаратной памяти, которая скрывает различия в доступе к разным аппаратным средствам и операционным системам и гарантирует, что программы Java могут достигать согласованных результатов при доступе к памяти на разных платформах.
аппаратная модель памяти
Прежде чем формально объяснить модель памяти Java, нам необходимо понять некоторые вещи на аппаратном уровне.
В аппаратной системе современных компьютеров вычислительная скорость ЦП очень высока, намного выше, чем скорость, с которой он считывает данные с носителя.Здесь много носителей, таких как магнитные диски, оптические диски, сетевые карты , память и т. д. Эти носители имеют очевидную характеристику - носитель данных, расположенный ближе к ЦП, имеет тенденцию быть меньше, дороже и быстрее, а носитель данных, расположенный дальше от ЦП, имеет тенденцию быть больше, дешевле и медленнее.
Поэтому, когда программа работает, большая часть времени ЦП тратится на дисковый ввод-вывод, сетевое взаимодействие и доступ к базе данных.Если мы не хотим, чтобы ЦП ждал напрасно, мы должны найти способ выжать вычислительные мощность ЦП, иначе это приведет к большому количеству отходов, и позволить ЦП обрабатывать несколько задач одновременно проще всего, и это также очень эффективное средство сжатия, которое мы часто называем «одновременное выполнение».
Однако не так просто заставить ЦП выполнять несколько задач одновременно, потому что все операции не могут быть выполнены только за счет вычислений ЦП, и часто требуется взаимодействие с памятью, например чтение данных операции, сохранение результатов операции и т. д.
Как мы уже говорили ранее, взаимодействие между ЦП и памятью часто очень медленное, поэтому нам нужно найти способ установить связь между ЦП и памятью, чтобы они могли достичь баланса, чтобы операция могла выполняться быстро. и это соединение мы часто называем «кешем».
Скорость кэш-памяти очень близка к скорости процессора, но его внедрение принесло новые проблемы.Современные процессоры часто имеют несколько ядер, каждое ядро имеет свой собственный кэш, и нет никакой разницы между несколькими ядрами.Если есть конкуренция для временных срезов они могут выполняться параллельно, поэтому обеспечение согласованности этих кэшей с данными в основной памяти становится сложной проблемой.
Чтобы решить проблему согласованности кеша, несколько ядер должны следовать некоторым протоколам при доступе к кешу и работать в соответствии с протоколом при операциях чтения и записи.Эти протоколы включают MSI, MESI, MOSI и т. д., которые определяют, когда они должны доступ к кешу.Основные принципы работы с данными, когда делать недействительными кеши и когда обращаться к данным в основной памяти.
При постоянном совершенствовании возможностей ЦП уровень кэш-памяти не может соответствовать требованиям, и постепенно создается многоуровневый кэш.
По порядку чтения данных и загруженности ЦП кэш ЦП можно разделить на кэш первого уровня (L1), кэш второго уровня (L2) и кэш третьего уровня (L3). Данные, хранящиеся в каждом уровне кэша, являются частью следующего уровня.
Техническая сложность и стоимость производства этих трех тайников относительно снижаются, а вместимость относительно увеличивается.
Итак, после наличия многоуровневого кеша работа программы становится такой:
Когда ЦП хочет прочитать часть данных, он сначала ищет из кеша первого уровня, если не найден, то из кеша второго уровня, если не нашел, то из кеша третьего уровня, если не нашел, то из основного Затем загружайте найденные данные в многоуровневый кеш по очереди и используйте соответствующие данные для поиска непосредственно из кеша в следующий раз.
Под загрузкой данных в кеш подразумевается не загрузка того, что используется, а загрузка непрерывных данных в память.Вообще говоря, он загружает непрерывные 64 байта.Поэтому, если вы обращаетесь к массиву длинного типа, когда при одном значении в массив загружается в кеш, остальные 7 элементов тоже загружаются в кеш, это и есть понятие "строка кеша".
Хотя строка кэша может значительно повысить эффективность работы программы, она создает новые проблемы в процессе доступа к общим переменным несколькими потоками, что является очень известным «ложным разделением».
Что касается вопроса псевдо-расшаривания, то здесь мы не будем об этом говорить, если вам интересно, вы можете прочитать предыдущий пост брата Тонга [Разное Что такое ложный обмен?] содержание, связанное с главой.
Кроме того, чтобы в полной мере использовать арифметический блок в ЦП, ЦП может выполнить оптимизацию выполнения не по порядку для входного кода, а затем реорганизовать результат выполнения не по порядку после вычисления, чтобы обеспечить результат и порядок.Результаты выполнения одинаковы, но это не гарантирует, что порядок вычисления каждого оператора в программе согласуется с порядком ввода кода.Поэтому, если вычислительная задача зависит от результатов другого вычисления задача, ее последовательность не может зависеть от порядка выполнения кода.
Подобно оптимизации выполнения ЦП вне очереди, JIT-компилятор виртуальной машины Java также имеет аналогичную оптимизацию переупорядочения инструкций.
Чтобы решить вышеупомянутые проблемы согласованности чтения и записи нескольких кэшей и оптимизации сортировки вне порядка, существует модель памяти, которая определяет спецификацию поведения многопоточных операций чтения и записи в системе с общей памятью.
Модель памяти Java
Модель памяти Java (JMM) представляет собой абстракцию более высокого уровня, основанную на модели аппаратной памяти, которая скрывает различия в доступе к памяти для различных аппаратных средств и операционных систем, тем самым позволяя Java-программам выполняться на различных платформах и достигать согласованного параллелизма.
Модель памяти Java определяет правила доступа для каждой переменной в программе, то есть базовые детали хранения и извлечения переменных в памяти и из памяти виртуальной машины. Упомянутые здесь переменные включают поля экземпляра и статические поля, но не включают локальные переменные и параметры метода, поскольку они являются частными для потоков, они не будут использоваться совместно, и, естественно, нет проблемы конкуренции.
Чтобы добиться лучшей производительности выполнения, модель памяти Java не ограничивает механизм выполнения в использовании определенных регистров или кэшей процессора для взаимодействия с основной памятью, а также не ограничивает права компилятора JIT на корректировку памяти. порядок выполнения кода.
Модель памяти Java предусматривает, что все переменные хранятся в основной памяти.Основная память здесь такая же, как имя, используемое при представлении оборудования.Их можно сравнить,но здесь относится только к части памяти в виртуальной машина.
Кроме основной памяти у каждого потока есть своя рабочая память, которую здесь можно сравнить с кешем ЦП. Копия копии основной памяти переменной, используемой потоком, хранится в рабочей памяти.Операции потока над переменной должны выполняться в рабочей памяти, включая чтение и присваивание, и не могут напрямую читать и записывать переменные в основная память Потоки не могут напрямую обращаться к переменным в рабочей памяти друг друга, и передача значений переменных между потоками должна осуществляться через основную память.
Связь между потоками, рабочей памятью и основной памятью показана на следующем рисунке:
Обратите внимание, что основная память, рабочая память, а также куча и стек в области памяти виртуальной машины Java представляют собой разные уровни разделения памяти. в куче, а рабочая память в основном соответствует некоторым областям в стеке виртуальной машины.
На более низком уровне основная память в основном соответствует аппаратной части памяти, рабочая память в основном соответствует кэш-памяти и регистровой части ЦП, но не является абсолютной, основная память может также существовать в кэш-памяти и регистрах, и рабочая память также может существовать в аппаратной памяти.
Взаимодействие между памятью
Что касается конкретного протокола взаимодействия между основной и рабочей памятью, модель памяти Java определяет следующие 8 конкретных операций, которые необходимо выполнить:
(1) блокировка, блокировка, переменная, действующая на основную память, которая идентифицирует переменную в основной памяти как состояние монопольного потока;
(2) разблокировка, разблокировка, воздействуя на переменные основной памяти, освобождает заблокированные переменные, а освобожденные переменные могут быть заблокированы другими потоками;
(3) читать, читать, воздействуя на переменные в основной памяти, он передает переменную из основной памяти в рабочую память для последующих операций загрузки;
(4) загрузка, загрузка, воздействуя на переменную рабочей памяти, помещает переменную, полученную из основной памяти операцией чтения, в переменную копию рабочей памяти;
(5) использовать, использовать переменную, которая воздействует на рабочую память, которая передает переменную в рабочей памяти механизму выполнения, и будет выполнять эту операцию всякий раз, когда виртуальная машина встречает инструкцию байт-кода, которая должна использовать значение переменная;
(6) assign, присваивание, воздействуя на переменную в рабочей памяти, она присваивает переменную, полученную от исполняющей машины, переменной в рабочей памяти, и эта операция используется всякий раз, когда виртуальная машина встречает инструкцию байт-кода, которая присваивает переменную переменной Переменная;
(7) хранение, хранение, воздействующая на рабочую память переменная, которая передает значение переменной из рабочей памяти в основную память для последующих операций записи;
(8) запись, запись, воздействуя на переменную основной памяти, помещает значение переменной, полученное из рабочей памяти операцией сохранения, в переменную основной памяти;
Если вы хотите скопировать переменную из основной памяти в рабочую память, вам необходимо выполнить операции чтения и загрузки последовательно.Точно так же, если вы хотите синхронизировать переменную из рабочей памяти обратно в основную память, вам необходимо выполнить последовательное сохранение и запись операции. Обратите внимание, что это только объясняет порядок и не говорит, что он должен быть непрерывным, то есть другие операции могут быть вставлены между чтением и загрузкой, а также между сохранением и записью. Например, доступ к переменным a и b в основной памяти может выполняться в следующем порядке:
читать a -> читать b -> загружать b -> загружать a.
Кроме того, модель памяти Java также определяет основные правила выполнения вышеуказанных 8 операций:
(1) Одна из операций чтения и загрузки, сохранения и записи не может появляться одна, то есть не допускается чтение из основной памяти, но рабочая память не принимает ее, или обратная запись из рабочей памяти. но основная память его не принимает;
(2) потоку не разрешается отбрасывать свою последнюю операцию присваивания, то есть, если переменная изменяется в рабочей памяти, это изменение должно быть синхронизировано обратно в основную память;
(3) потоку не разрешено синхронизировать переменную из рабочей памяти обратно в основную память без какой-либо причины (то есть операция присваивания не выполнялась);
(4) Новая переменная должна быть рождена в основной памяти.Не допускается прямое использование неинициализированной (загрузить или присвоить) переменной в рабочей памяти.Иными словами, операции использования и сохранения над переменной должны быть выполнены до , загружать и назначать операции;
(5) Переменная может быть заблокирована только одним потоком одновременно, но операция блокировки может выполняться одним и тем же потоком несколько раз.После многократного выполнения блокировки переменная может быть разблокирована только путем выполнения одного и того же числа. операций разблокировки.
(6.
(7) Если переменная не заблокирована операцией блокировки, ей не разрешается выполнять операцию разблокировки, а также не разрешается разблокировать переменную, заблокированную другими потоками;
(8) Перед выполнением операции разблокировки переменной переменная должна быть синхронизирована обратно в основную память, то есть выполняются операции сохранения и записи;
Обратите внимание, что блокировка и разблокировка здесь являются основой для реализации синхронизации Java не открывает пользователям операции блокировки и разблокировки напрямую, но предоставляет две инструкции более высокого уровня для неявного использования этих двух операций, а именно monitorenter и moniterexit.
Атомарность, Видимость, Порядок
Модель памяти Java предназначена для решения проблемы согласованности общих переменных в многопоточной среде, так что же включает в себя согласованность?
Непротиворечивость в основном включает в себя три характеристики: атомарность, видимость и упорядоченность.Давайте посмотрим, как модель памяти Java реализует эти три характеристики.
(1) атомарность
Атомарность означает, что после запуска операции она будет выполняться до конца, не прерываясь другими потоками.Эта операция может быть одной операцией или несколькими операциями.
Атомарные операции, непосредственно гарантированные моделью памяти Java, включают чтение, загрузку, пользователя, назначение, сохранение и запись.Грубо можно считать, что чтение и запись переменных базового типа являются атомарными.
Если приложению требуется более широкий диапазон атомарности, модель памяти Java также предоставляет две операции, блокировку и разблокировку, чтобы выполнить это требование.Хотя эти две операции нельзя использовать напрямую, мы можем использовать их более конкретную реализацию, синхронизированную для выполнения.
Следовательно, операции между синхронизированными блоками также являются атомарными.
(2) Видимость
Видимость означает, что когда поток изменяет значение общей переменной, другие потоки могут немедленно воспринять это изменение.
Модель памяти Java реализуется путем синхронизации с основной памятью после изменения изменений и обновления значения переменной из основной памяти до того, как переменная будет прочитана.Это зависит от основной памяти, будь то обычная переменная или переменная.
Основное различие между обычными переменными и изменчивыми переменными заключается в том, синхронизируются ли они обратно в основную память сразу после модификации и удаляются ли они из основной памяти непосредственно перед каждым чтением. Так что можно сказать, что volatile-переменные гарантируют видимость переменных в многопоточной среде, но обычные переменные этого гарантировать не могут.
Кроме volatile есть еще два ключевых слова, которые тоже гарантируют видимость, это synchronized и final.
Видимость синхронизированных получается по правилу «перед выполнением операции разблокировки переменной переменная должна быть синхронизирована обратно в основную память, то есть выполнять операции сохранения и записи».
Видимость final означает, что как только поле, измененное final, инициализируется в конструкторе, другие потоки могут видеть final поле.
(3) упорядоченный
Естественный порядок в программах на Java можно описать одним предложением: если наблюдается в этом потоке, все операции упорядочены, если наблюдается в другом потоке, все операции не в порядке.
Первая половина предложения относится к семантике последовательного выполнения в потоке, а вторая половина предложения относится к явлению «переупорядочения инструкций» и «задержки синхронизации между рабочей памятью и основной памятью».
Java предоставляет два ключевых слова volatile и synchronized для обеспечения упорядоченности.
Volatile по своей природе упорядочен, потому что запрещает изменение порядка.
Порядок синхронизации определяется правилом, согласно которому «только одному потоку разрешено блокировать переменную одновременно».
Бывает-до
Если упорядочение модели памяти Java опирается только на volatile и synchronized, то некоторые операции станут очень многословными, но мы не чувствуем этого при написании параллельного кода Java, потому что язык Java естественным образом определяет принцип «происходит первым»: очень важно, и, опираясь на этот принцип, мы можем легко судить, могут ли возникнуть конфликты конкуренции между двумя операциями в параллельной среде.
«Происходит первым» означает, что операция A выполняется раньше операции B, после чего влияние операции A может быть воспринято операцией B, включая изменение значения переменных в разделяемой памяти, отправку сообщений, вызов методов и т. д.
Давайте посмотрим, что происходит. Первые принципы, определенные моделью памяти Java:
(1) Принцип процедурного порядка
Внутри потока программа выполняется в том порядке, в котором она написана. Операции, написанные в начале, выполняются раньше, чем операции, написанные в конце. Точнее, это порядок потока управления, а не порядок кода, потому что необходимо учитывать ответвления, петли и т. д.
(2) Принцип блокировки монитора
Операция разблокировки выполняется перед последующей операцией блокировки той же блокировки.
(3) изменчивый принцип
Операция записи переменной Volatile сначала происходит при операции чтения переменной.
(4) Принцип запуска потока
Операция start() в потоке предшествует любой операции, происходящей в потоке.
(5) Принцип прекращения резьбы
Все операции в потоке выполняются в первую очередь при обнаружении завершения потока.Завершение потока можно определить по возвращаемому значению функций Thread.join() и Thread.isAlive().
(6) принцип прерывания потока
Вызов метода interrupt() потока происходит первым в коде потока, чтобы обнаружить возникновение события прерывания.Вы можете использовать метод Thread.interrupted(), чтобы определить, происходит ли прерывание.
(7) Принцип прекращения объекта
Завершение инициализации объекта (конец выполнения конструктора) происходит сначала в начале его метода finalize().
(8) Принцип транзитивности
Если операция А происходит раньше операции Б, а операция Б происходит раньше операции С, то операция А выполняется раньше операции С.
Здесь не упоминается необходимая связь между «происходить раньше» и «происходить раньше во времени».
Например, следующий код:
int a = 0;
// 操作A:线程1对进行赋值操作
a = 1;
// 操作B:线程2获取a的值
int b = a;
Если поток 1 сначала присваивает значение a в хронологическом порядке, а затем поток 2 получает значение a, значит ли это, что операция A выполняется раньше операции B?
Очевидно, что нет, потому что поток 2 все еще может считать значение в своей рабочей памяти или поток 1 не обновил значение возврата в основную память.В это время значение, прочитанное потоком 2, может все еще быть 0.
Следовательно, «происходит первым во времени» не обязательно «происходит первым».
Давайте посмотрим на другой пример:
// 同一个线程中
int i = 1;
int j = 2;
Согласно первому принципу процессуального порядка,int i = 1;
происходит доint j = 2;
, но из-за оптимизации процессора может вызватьint j = 2;
Выполнить сначала, но это не влияет на правильность принципа «происходит раньше», потому что мы не воспринимаем это в этой теме.
Следовательно, «происходит первым» не обязательно «происходит первым во времени».
Суммировать
(1) Архитектура аппаратной памяти требует создания модели памяти для обеспечения корректного доступа к разделяемой памяти в многопоточной среде;
(2) модель памяти Java определяет правила обеспечения согласованности общих переменных в многопоточной среде;
(3) Модель памяти Java обеспечивает 8 основных операций для взаимодействия между рабочей и основной памятью: блокировка, разблокировка, чтение, загрузка, использование, назначение, сохранение, запись;
(4) модель памяти Java предоставляет некоторые реализации атомности, видимости и упорядоченности;
(5) Восемь принципов, которые возникают первыми: принцип порядка выполнения программы, принцип блокировки монитора, принцип изменчивости, принцип запуска потока, принцип завершения потока, принцип прерывания потока, принцип завершения объекта и принцип транзитивности;
(6) Предварительное возникновение не равно временному предшествующему возникновению;
пасхальные яйца
Модель памяти Java — очень важная концепция в Java. Понимание этого очень полезно для нас, чтобы писать многопоточный код и понимать природу многопоточности.Автор собрал здесь полезную информацию для всех.
"Глубокое понимание виртуальной машины Java"
Искусство параллельного программирования на Java
"Углубленное понимание модели памяти Java"
Обратите внимание на мою общедоступную учетную запись «Tong Ge Reading Source Code» и ответьте на «JMM», чтобы получить эти три книги.
Добро пожаловать, чтобы обратить внимание на мою общедоступную учетную запись «Брат Тонг читает исходный код», просмотреть больше статей в серии исходного кода и поплавать в океане исходного кода с братом Тонгом.