В серии статей о Java JVM друг спросил, зачем нам JVM: разве виртуальная машина Java уже не обработана за нас? Точно так же при изучении модели памяти Java возникает тот же вопрос: зачем изучать модель памяти Java? Их ответ тот же: это позволяет нам лучше понять основные принципы и писать более эффективный код.
Что касается модели памяти Java, то она является необходимым условием для глубокого понимания параллельного программирования на Java. Это очень полезно для безопасности потоков и синхронной асинхронной обработки в последующей многопоточности.
аппаратная архитектура памяти
Прежде чем изучать модель памяти Java, сначала разберитесь с моделью аппаратной памяти компьютера. Все мы знаем, что процессоры и запоминающие устройства компьютера имеют разницу в скорости работы на несколько порядков. Процессор не всегда может дождаться запоминающего устройства компьютера, поэтому нет возможности показать преимущества процессора.
Поэтому для «выжимания» производительности обработки и достижения эффекта «высокого параллелизма» между процессором и запоминающим устройством в качестве буфера добавляется кеш (кеш).
Скопируйте данные, которые должна использовать операция, в кэш, чтобы операцию можно было выполнить быстро. Когда операция завершена, результат в кеше записывается в основную память, благодаря чему оператору не приходится ждать операций чтения и записи основной памяти.
Каждый процессор имеет свой собственный кеш, при совместной работе с одним и тем же блоком, когда несколько процессоров работают одновременно, данные несогласованны, поэтому для гарантии требуется «Cache Consistency Protocol». Например, MSI, Mesi и т.д.
Модель памяти Java
Модель памяти Java — это модель памяти Java, или сокращенно JMM. Он используется для защиты различий в доступе к памяти различного оборудования и операционных систем, чтобы Java-программы могли достигать согласованных эффектов доступа к памяти на всех платформах.
JMM определяет абстрактную связь между потоками и основной памятью: общие переменные между потоками хранятся в основной памяти (основной памяти), каждый поток имеет частную локальную память (локальную память), а локальная память хранит потоки для чтения/записи копий. общих переменных. Локальная память является абстракцией JMM и на самом деле не существует. Он охватывает кеши, буферы записи, регистры и другие оптимизации оборудования и компилятора.
JMM и структура памяти Java не являются одним и тем же уровнем разделения памяти, и они в основном не связаны. Если вы должны неохотно соответствовать, из определения переменных, основной памяти и рабочей памяти, основная память в основном соответствует части данных экземпляра объекта в куче Java, а рабочая память соответствует части стека виртуальной машины.
Основная память: объекты экземпляра Java в основном хранятся, а объекты экземпляра, созданные всеми потоками, хранятся в основной памяти, независимо от того, является ли объект экземпляра переменной-членом или локальной переменной (также известной как локальная переменная) в методе. и, конечно же, он также включает информацию об общем классе, константы, статические переменные. Общие области данных, несколько потоков, обращающихся к одной и той же переменной, могут столкнуться с проблемами безопасности потоков.
Рабочая память: в основном хранится вся информация о локальных переменных текущего метода (рабочая память хранит копию копии переменной в основной памяти), каждый поток может обращаться только к своей собственной рабочей памяти, то есть к локальным переменным в потоке. невидимы для других потоков Да, даже если два потока выполняют один и тот же фрагмент кода, каждый из них будет создавать локальные переменные, принадлежащие текущему потоку, в своей собственной рабочей памяти, конечно, включая индикатор номера строки байт-кода и связанную информацию о родном методе . Поскольку рабочая память является частными данными каждого потока, потоки не могут обращаться к рабочей памяти друг с другом, поэтому данные, хранящиеся в рабочей памяти, не имеют проблем с безопасностью потоков.
Прямое сравнение между моделью JMM и аппаратной моделью можно упростить, как показано на следующем рисунке:
Взаимодействие между памятью
Копия копии основной памяти переменных, используемых потоком, хранится в рабочей памяти потока.Все операции над переменными потоком должны выполняться в рабочей памяти и не могут напрямую читать и записывать переменные в основная память. Разные потоки не могут напрямую обращаться к переменным в рабочей памяти друг друга, и передачу значений переменных между потоками нужно выполнять через основную память.
Как показано на рисунке выше, в локальной памяти A и B есть копии общей переменной x в основной памяти, и начальное значение равно 0. После выполнения потока A значение x обновляется до 1 и сохраняется в локальной памяти A. Когда потоку A и потоку B необходимо взаимодействовать, поток A сначала обновляет значение x=1 из локальной памяти в основную память, и значение x в основной памяти становится равным 1. Затем поток B переходит в основную память для чтения обновленного значения x, и значение x локальной памяти потока B также становится равным 1.
Во время этого взаимодействия модель памяти Java определяет 8 операций для завершения, и реализация виртуальной машины должна гарантировать, что каждая операция является атомарной и не может быть разделена снова (за исключением типов double и long).
- lock: переменная, которая воздействует на основную память и идентифицирует переменную как состояние монопольного потока.
- разблокировать (разблокировать): переменная, действующая на основную память, она освобождает переменную в заблокированном состоянии, а освобожденная переменная может быть заблокирована другими потоками.
- read (чтение): переменная, действующая на основную память, которая передает значение переменной из основной памяти в рабочую память потока для последующих действий загрузки.
- load: переменная, воздействующая на рабочую память, которая помещает значение переменной, полученное операцией чтения из основной памяти, в переменную копию рабочей памяти.
- использование: переменная, воздействующая на рабочую память, которая передает значение переменной в рабочей памяти механизму выполнения, который будет выполняться всякий раз, когда виртуальная машина встречает инструкцию байт-кода, которая должна использовать значение переменной.
- assign (назначение): переменная, которая воздействует на рабочую память, она присваивает значение, полученное от исполняющего движка, переменной в рабочей памяти, и выполняет эту операцию всякий раз, когда виртуальная машина встречает инструкцию байт-кода, которая присваивает значение переменной.
- store (хранение): переменная, действующая на рабочую память, которая передает значение переменной из рабочей памяти в основную память для последующих операций записи.
- write (запись): переменная, действующая на основную память, которая помещает значение переменной, полученное из рабочей памяти операцией сохранения, в переменную основной памяти.
Если вам нужно скопировать переменную из основной памяти в рабочую память, она должна будет выполнять операции чтения и загрузки последовательно, если мы хотим, чтобы переменная синхронизировалась обратно в основную память из рабочей памяти, необходимо выполнить операции сохранения и записи последовательно. Обратите внимание, что модель памяти Java требует, чтобы только две описанные выше операции выполнялись последовательно, но не гарантируется, что они будут выполняться непрерывно. То есть между чтением и загрузкой, между сохранением и записью можно вставить другие инструкции, такие как переменные основной памяти a, b при доступе, порядок чтения, скорее всего, будет запускать a, b читать, загружать b, загружать a. Кроме того, модель памяти Java также предусматривает соблюдение следующих правил при выполнении вышеописанных 8 основных операций.
- Одна из операций чтения и загрузки, сохранения и записи не может появляться одна, то есть переменная не может быть прочитана из основной памяти, но не принята в рабочую память, или обратная запись инициирована из рабочей памяти, но не принимается основной памятью.
- Поток не может отменить свою последнюю операцию присваивания, то есть после того, как переменная была изменена в рабочей памяти, он должен синхронизировать изменение обратно в основную память.
- Потоку не разрешено синхронизировать данные из рабочей памяти потока обратно в основную память без какой-либо причины (операция присваивания не выполнялась).
- Новая переменная может «родиться» только в основной памяти, а использовать неинициализированные (загрузить или присвоить) переменную непосредственно в рабочей памяти нельзя, т. е. перед реализацией операций использования и сохранения над переменной, это должно быть Операции назначения и загрузки были выполнены первыми.
- Переменная может быть заблокирована только одним потоком одновременно, но операция блокировки может повторяться одним и тем же потоком несколько раз.После многократного выполнения блокировки переменная будет разблокирована только после такого же количества операций разблокировки. выполненный.
- Если операция блокировки выполняется над переменной, значение переменной в рабочей памяти будет очищено.Прежде чем исполнительный механизм использует переменную, необходимо повторно выполнить операцию загрузки или назначения, чтобы инициализировать значение переменной.
- Если переменная ранее не была заблокирована операцией блокировки, не разрешается выполнять над ней операцию разблокировки или разблокировать переменную, заблокированную другим потоком.
- Перед выполнением операции разблокировки переменной переменная должна быть синхронизирована обратно в основную память (выполняются операции сохранения и записи).
Специальные правила для переменных типа long и double
Модель памяти Java требует, чтобы восемь операций блокировки, разблокировки, чтения, загрузки, назначения, использования, хранения и записи были атомарными, но для 64-битных типов данных (длинных или двойных) в модели определено относительно свободное определение. Оговорено, что виртуальной машине разрешено разделять операции чтения и записи 64-битных данных, которые не модифицируются volatile на две 32-битные операции, то есть виртуальной машине разрешено выбирать загрузку, сохранение, чтение , и пишут, что не гарантируют 64-битные типы данных.Атомарность этих 4-х операций, неатомарный контракт long и double.
Если тип double или long не объявлен как volatile в случае многопоточности, может быть значение "полупеременной", то есть ни исходное значение, ни измененное значение.
Хотя спецификация Java допускает описанную выше реализацию, атомарные операции в основном используются в коммерческих виртуальных машинах, поэтому практически невозможно прочитать «половину переменной» в повседневном использовании.
резюме
Этот урок посвящен модели памяти Java, а также шагам и операциям взаимодействия с памятью. В следующей статье мы сосредоточимся на нескольких особенностях и принципах модели памяти Java. Добро пожаловать в общедоступную учетную запись WeChat «Program New Horizon», чтобы получать последние обновления статей как можно скорее.
Оригинальная ссылка: "Подробная модель памяти Java (JMM)》
Серия статей "Интервьюер":
- "Подробное объяснение структуры памяти JVM》
- "Интервьюер, перестаньте спрашивать меня "Механизм сборки мусора Java GC"》
- "Интервьюер, Структура памяти Java8 JVM изменилась, постоянное преобразование в метапространство》
- "Интервьюер, перестаньте спрашивать меня "сборщик мусора Java"》
- "Загрузчик классов виртуальной машины Java и родительский механизм делегирования》
- "Подробная модель памяти Java (JMM)》