Введение в JMM
Модель памяти Java, называемая JMM, представляет собой серию платформ виртуальных машин Java, которые предоставляют разработчикам единую гарантию видимости памяти в многопоточной среде, возможность ее переупорядочивания и другие вопросы, не зависящие от конкретных платформ. . (Это может быть неоднозначно в терминологии с распределением памяти во время выполнения Java, которое относится к областям памяти, таким как куча, область методов, стек потоков и т. д.).
Существует множество стилей параллельного программирования.Помимо CSP (последовательный процесс связи), акторов и других моделей, наиболее знакомой должна быть модель разделяемой памяти, основанная на потоках и блокировках. В многопоточном программировании существует три типа проблем параллелизма, о которых следует знать:
- атомарность
- видимость
- изменение порядка
Атомарность относится к тому, могут ли другие потоки видеть промежуточное состояние или вмешиваться, когда один поток выполняет составную операцию. Типичная проблема — i++ Два потока одновременно выполняют операции ++ над общей памятью кучи, а реализация операций ++ в JVM, среде выполнения и ЦП может быть составной операцией, например, с точки зрения JVM. инструкции.Это чтение значения i из памяти кучи в стек операндов, добавление единицы, а затем запись обратно в i памяти кучи.Во время этих операций, если нет правильной синхронизации, другие потоки также могут выполняться одновременно, что может привести к потере данных и т. д. Общие проблемы атомарности, также известные как условия гонки, основаны на возможном недопустимом результате, таком как чтение-изменение-запись. Проблемы с видимостью и изменением порядка возникают из-за оптимизации системы.
Из-за серьезного несоответствия между скоростью выполнения ЦП и скоростью доступа к памяти, чтобы оптимизировать производительность, основываясь на принципах локальности, таких как локальность во времени и пространственная локальность, ЦП добавляет несколько уровней кэшей между ЦП и памяти.Когда данные должны быть извлечены, ЦП сначала обращается к кешу, чтобы узнать, существует ли соответствующий кеш, и если он существует, он вернется напрямую.Если он не существует, он будет извлечен из памяти и хранится в кэше. Теперь многоядерные процессоры в основном стали стандартом.В это время каждый процессор имеет свой собственный кеш, что связано с проблемой когерентности кеша.ЦП имеют разные модели согласованности с разной силой и самую сильную безопасность согласованности.Наивысший, также в соответствии с наш последовательный режим мышления, но с точки зрения производительности будет много накладных расходов из-за необходимости скоординированного взаимодействия между различными процессорами.
Схематическая диаграмма типичной структуры кэша ЦП выглядит следующим образом.
Цикл инструкций ЦП обычно представляет собой выборку инструкций, разбор инструкций для чтения данных, выполнение инструкций и запись данных обратно в регистры или память. При последовательном выполнении инструкций часть чтения и хранения данных занимает много времени, поэтому ЦП обычно использует метод конвейера инструкций для одновременного выполнения нескольких инструкций для повышения общей пропускной способности, как и заводской конвейер.
Чтение данных и запись данных обратно в память не на порядок быстрее, чем выполнение инструкций, поэтому ЦП использует в качестве кэшей и буферов регистры и кэши. При чтении данных из памяти считывается строка кэша. читать, чтобы читать блок). Модуль обратной записи данных поместит запрос на хранение в буфер сохранения и продолжит выполнение следующего этапа цикла инструкций, когда старых данных нет в кеше.Если они есть в кеше, кеш будет обновлен, и данные в кеше будут обновляться в соответствии с определенными стратегиями сброса в память.
public class MemoryModel {
private int count;
private boolean stop;
public void initCountAndStop() {
count = 1;
stop = false;
}
public void doLoop() {
while(!stop) {
count++;
}
}
public void printResult() {
System.out.println(count);
System.out.println(stop);
}
}
Когда приведенный выше код выполняется, мы можем подумать, что count = 1 будет выполнен до того, как stop = false, что правильно в идеальном состоянии, показанном на диаграмме выполнения ЦП выше, но не при рассмотрении верхнего регистра и кэш-буфера. , например, сама остановка находится в кеше, а счетчик нет, то буфер записи счетчика может быть сброшен в память после обновления остановки до того, как буфер записи будет записан обратно.
Кроме того, ЦП и компилятор (обычно называемый JIT для Java) могут изменять порядок выполнения инструкций, например, в приведенном выше коде count = 1 и stop = false не имеют зависимостей, поэтому ЦП и компилятор могут изменить оба. Порядок выполнения одинаков в программе, выполняемой одним потоком, который также является последовательностью «как если бы», гарантированной ЦП и компилятором (независимо от того, как изменен порядок выполнения, результат выполнения одна нить остается неизменной). Поскольку большая часть выполнения программы выполняется в однопоточном режиме, такая оптимизация приемлема и приводит к значительному повышению производительности. Но в случае многопоточности могут возникнуть неожиданные результаты, если не будут выполнены необходимые операции синхронизации. Например, после того как поток T1 выполнит метод initCountAndStop, поток T2 выполнит printResult, и результатом может быть 0, ложь, 1, ложь или 0, истина. Если поток T1 сначала выполняет doLoop(), а поток T2 выполняет initCountAndStop секундой позже, T1 может выйти из цикла или никогда не увидеть модификацию остановки из-за оптимизации компилятора.
Из-за различных проблем в многопоточной ситуации, упомянутых выше, порядок выполнения программы в многопоточности больше не является порядком выполнения и результатом базового механизма. Язык программирования должен дать разработчикам гарантию. Эта гарантия является просто модификацией Он виден другим потокам одновременно, поэтому язык Java предлагает модель памяти Java, которая является моделью памяти Java, и разработчики языка Java, JVM, компилятора и т. д. должны реализовать ее в соответствии с к условностям этой модели. Java предоставляетVolatile, синхронизированный, окончательный и другие механизмы, помогающие разработчикам обеспечивать корректность многопоточных программ на всех процессорных платформах.
До JDK 1.5 у модели памяти Java были серьезные проблемы: например, в старой модели памяти поток мог видеть значение по умолчанию конечного поля после выполнения конструктора, а запись изменчивого поля могла быть несовместима с несовместимой. - Чтение и запись переупорядочивания изменчивых полей.
Поэтому в JDK1.5 была предложена новая модель памяти через JSR133, чтобы исправить предыдущие проблемы.
правила переупорядочивания
энергозависимые и контрольные блокировки
Можно ли перезаказать | вторая операция | вторая операция | вторая операция |
---|---|---|---|
первое действие | Нормальное чтение/обычная запись | volatile чтение/монитор ввода | выход из энергозависимой записи/мониторинга |
Нормальное чтение/обычная запись | No | ||
voaltile чтение/монитор ввода | No | No | No |
выход из энергозависимой записи/мониторинга | No | No |
Среди них обычное чтение относится к getfield, getstatic, arrayload энергонезависимого массива, а обычная запись относится к putfield, putstatic, arraystore энергонезависимого массива.
Чтение и запись изменчивых данных — это getfield, getstatic и putfield, putstatic или volatile field соответственно.
monitorenter относится к входу в синхронизированный блок или метод, а monitorexist относится к выходу из синхронизированного блока или метода.
Нет в приведенной выше таблице означает, что две операции не могут быть переупорядочены, например (обычная запись, запись в энергозависимую память) означает, что запись энергонезависимого поля не может быть переупорядочена с записью любого последующего энергозависимого поля. Когда нет Нет, переупорядочивание разрешено, но JVM должна гарантировать минимальную безопасность — прочитанное значение является либо значением по умолчанию, либо чем-то, записанным другим потоком (64-битные двойные и длинные операции чтения и записи — это особый случай, когда Без изменчивого оформления операции чтения и записи не гарантируются атомарностью, и нижележащий уровень может разделить их на две отдельные операции).
конечное поле
final поля имеют два дополнительных специальных правила
-
Ни запись конечного поля (в конструкторе), ни запись ссылки на сам объект конечного поля не могут быть переупорядочены с последующей (вне конструктора) записью объекта, содержащего конечное поле. Например, следующие операторы не могут быть переупорядочены.
-
Первая загрузка конечного поля не может быть переупорядочена с записью объекта, содержащего конечное поле, например, следующий оператор не позволяет переупорядочивать
барьер памяти
Все процессоры поддерживают определенные барьеры или барьеры памяти для управления переупорядочением и видимостью данных между различными процессорами. Например, когда ЦП записывает данные обратно, он помещает запрос на сохранение в буфер записи и ждет, пока он будет сброшен в память.Установив барьер, можно предотвратить переупорядочивание запроса на сохранение с другими запросами, чтобы гарантировать видимость данных. В качестве аналогии со шлагбаумом можно привести пример из жизни, например, при подъеме на рампу метро лифта все заходят в лифт по порядку, но некоторые обходят с левой стороны, поэтому порядок при выходе из лифта другой , Если большой багаж заблокирован (шлагбаум), люди сзади не могут его обойти :). Кроме того, барьер здесь и барьер записи, используемый в GC, — разные понятия.
Классификация барьеров памяти
Почти все процессоры поддерживают некую грубую барьерную инструкцию, обычно называемую забором (fence, забор), которая может гарантировать, что инструкции загрузки и сохранения, инициированные до забора, могут быть строго упорядочены с загрузкой и сохранением после забора. Обычно он делится на следующие четыре барьера в зависимости от цели.
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
Перед загрузкой данных Load2 и последующими загрузками убедитесь, что данные Store1 видны другим процессорам (например, при сбросе в память). StoreLoad Barrier не позволяет нагрузкам читать старые данные вместо данных, недавно записанных другими процессорами.
Почти все современные мультипроцессоры требуют StoreLoad, StoreLoad обычно является самым дорогим, а StoreLoad имеет эффект трех других барьеров, поэтому StoreLoad можно использовать в качестве общего (но более дорогого) барьера.
Следовательно, используя указанный выше барьер памяти, можно реализовать правила переупорядочения в приведенной выше таблице.
необходимый барьер | вторая операция | вторая операция | вторая операция | вторая операция |
---|---|---|---|---|
первое действие | нормальное чтение | Обычная запись | volatile чтение/монитор ввода | выход из энергозависимой записи/мониторинга |
нормальное чтение | LoadStore | |||
нормальное чтение | StoreStore | |||
voaltile чтение/монитор ввода | LoadLoad | LoadStore | LoadLoad | LoadStore |
выход из энергозависимой записи/мониторинга | StoreLoad | StoreStore |
Чтобы поддерживать правила финальных полей, необходимо добавить барьер к окончательному написанию.
x.finalField = v; StoreStore; sharedRef = x;
Вставьте барьер памяти
На основе приведенных выше правил можно добавить барьеры к обработке volatile-полей и синхронизированных ключевых слов, чтобы соответствовать правилам модели памяти.
- Вставьте барьер StoreStore перед энергозависимым хранилищем
- Вставить в StoreStore после того, как будут записаны все окончательные поля, но до возврата конструктора
- Вставьте барьер StoreLoad после энергозависимого хранилища
- Вставьте барьеры LoadLoad и LoadStore после энергозависимых нагрузок
- Правила входа монитора и загрузки энергозависимой памяти согласованы, а правила выхода монитора и сохранения энергозависимой памяти согласованы.
HappenBefore
Различные барьеры памяти, упомянутые выше, относительно сложны для нижнего уровня для разработчиков, поэтому JMM может использовать ряд правил отношения частичного порядка HappenBefore для иллюстрации, чтобы гарантировать, что поток, выполняющий операцию B, видит результат операции A (независимо от независимо от того, выполняются ли A и B в одном и том же потоке), то между A и B должно выполняться отношение HappenBefore, иначе JVM может произвольно изменить их порядок.
Список правил HappenBefore
Правила HappendBefore включают
- Правило порядка выполнения программы: если операция A предшествует операции B в программе, то операция A будет выполняться перед операцией B в том же потоке.
- Правило блокировки монитора: операция блокировки для блокировки монитора должна быть выполнена до операции блокировки для той же блокировки монитора.
- правила volatile-переменной: запись в volatile-переменную должна выполняться перед чтением переменной
- Правила запуска потока: вызов Thread.start в потоке должен быть выполнен до того, как в этом потоке будут выполнены какие-либо операции.
- Правило завершения потока: любая операция в потоке должна быть выполнена до того, как другие потоки обнаружат, что поток завершен.
- Правила прерывания: когда поток вызывает прерывание в другом потоке, он должен сделать это до того, как прерванный поток обнаружит прерывание.
- Транзитивность: если операция А выполняется до операции В, а операция В выполняется до операции С, то операция А выполняется до операции С.
где блокировки отображения имеют ту же семантику памяти, что и блокировки монитора, а атомарные переменные имеют ту же семантику памяти, что и volatile. Приобретение и освобождение блокировок, а также операции чтения и записи энергозависимых переменных удовлетворяют соотношению общего порядка, поэтому энергозависимые записи могут использоваться перед последующими энергозависимыми операциями чтения.
Вы можете комбинировать несколько правил HappenBefore выше.
Например, после того, как поток A входит в блокировку монитора, операция перед снятием блокировки монитора выполняется HappenBefore в операции освобождения монитора в соответствии с правилом последовательности программ, а операция освобождения монитора HappenBefore выполняется в последующей операции захвата того же монитора потоком B. блокировка, операция HappenBefore и операция в потоке B.
читать далее
Горячий ремонт в действии — самая подробная статья в истории, настоятельно рекомендуется
Выбор изображения Android для обрезки шаг за шагом
Мой сайт, которым я руковожу - научу вас играть с функциями и переменными Python
Верь в себя, нет ничего невозможного, только неожиданное
Публичный аккаунт WeChat: Отдел исследований и разработок Терминала