Глубокое понимание изменчивости

Java задняя часть JVM переводчик
Глубокое понимание изменчивости
Прежде чем разбираться с volatile, давайте взглянем на режим работы ЦП:



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

Чтобы решить вышеупомянутые три проблемы, JVM предлагает JMM (модель памяти JAVA), чтобы обеспечить согласованность результатов выполнения кода на каждой платформе.Цель состоит в том, чтобы заставить Java-программы достигать согласованных результатов на разных платформах.

Спецификация JMM:
Принцип «случиться до»:
1. Принцип порядка программы: гарантированная семантическая сериализация внутри потока
2. Правило volatile: запись volatile-переменных происходит перед чтением, что обеспечивает видимость volatile-переменных
3. Правила блокировки: разблокировка должна произойти до блокировки
4. Транзитивность: A предшествует B, B предшествует C и A должно предшествовать C.
5. Метод start() потока предшествует каждому его действию.
6. Все действия потока до завершения потока
7. Потоки прерываются до того, как прерывается код
8. Конструктор объекта выполняется и завершается до метода finalize().

Оптимизация для volatile:
Volatile гарантирует, что изменения видны другим потокам. То есть, после того, как разделяемая переменная будет изменена, она обязательно будет сброшена обратно в оперативную память для уведомления других потоков, но для того, чтобы внутренний блок процессора работал эффективно, процессор перетасует входной код, т.е. есть, переупорядочить инструкции. Если с volatile не обращаться целенаправленно, то очевидно, что видимость volatile не будет иметь особого смысла. Определенность результатов не может быть гарантирована.
Над volatileJVM было проделано много работы:
Что касается рабочей памяти (кэш для оборудования), JMM определяет 8 операций для выполнения:
  • lock (блокировка): действует в основной памяти и помечает переменную как эксклюзивную для потока.
  • разблокировать (разблокировать): воздействует на основную память, чтобы освободить заблокированную переменную.
  • чтение: Воздействует на основную память, перенося переменную из основной в рабочую память для последующих загрузок.
  • Загрузить (load): воздействовать на рабочую память и поместить переменные, полученные операцией чтения, в переменную копию рабочей памяти.
  • use (использовать): Воздействовать на рабочую память и передавать переменную в рабочей памяти механизму выполнения.
  • Assign (назначение): воздействует на рабочую память и присваивает значение, принятое механизмом выполнения, переменной в рабочей памяти.
  • store (хранение): воздействует на рабочую память и передает значение переменной из рабочей памяти в основную память для последующих операций записи.
  • write (запись): воздействует на основную память, помещая значение, полученное из рабочей памяти в результате операции сохранения, обратно в основную память.
Операции в 8 следующие:
  • Загрузка и чтение, сохранение и запись не могут появляться по отдельности.
  • Поток не может отменить свою последнюю операцию присваивания, т. е. переменные, измененные в рабочей памяти, и должны быть синхронизированы обратно в основную память.
  • Не позволяйте потоку синхронизировать данные из рабочей памяти обратно в основную память без причины (без операции назначения).
  • Новая переменная может быть создана только в основной памяти.
  • Переменная может быть заблокирована только одним потоком за раз. Замок может быть заблокирован несколько раз одним и тем же потоком, но должен быть разблокирован одинаковое количество раз. Эта переменная будет разблокирована.
  • Выполните операцию блокировки в переменной. Значение переменной в рабочей памяти потока будет сначала очищена. Перед выполнением двигателя используются эта переменная, операция нагрузки или назначения должна быть восстановлена.
  • Переменная заблокирована, и другим потокам не разрешено выполнять разблокировку. Также не разрешается выполнять переменные разблокировки, которые заблокированы другими потоками. То есть, если поток блокирует себя, только он может его разблокировать.
  • Прежде чем переменная будет разблокирована, данные в рабочей памяти должны быть синхронизированы с основной памятью.
Эти восемь операций и правила их использования определяют стратегию синхронизации переменных между оперативной и основной памятью.

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

JVM также будет мешать перестановке volatile во время компиляции.Правила вмешательства следующие:


  1. Если вторая операция является энергозависимой записью, независимо от того, что представляет собой первая операция, ее нельзя переупорядочить.
  2. Если первая операция является непостоянным чтением, независимо от того, что представляет собой вторая операция, ее нельзя переупорядочить.
  3. Нестабильные операции записи и чтения не могут быть переупорядочены.

Чтобы достичь этой семантики, JVM вставляет барьеры памяти в последовательность инструкций, чтобы запретить определенные типы переупорядочивания инструкций процессора при генерации байт-кода.Для компилятора минимальное количество барьеров, вставленных для всех ЦП.Решение практически невозможно, следующее - JMM стратегия вставки барьера памяти, основанная на консервативной стратегии:
  1. Вставьте барьер Storestore перед каждой записью Volatile.
  2. Вставьте барьер для хранилища загрузки после каждой летучих напитков
  3. Вставьте барьер LoadLoad после каждого чтения volatile
  4. Вставьте барьер LoadStore после каждого чтения volatile

Вот что такое барьер памяти: На аппаратном уровне существует два типа барьеров памяти: барьер загрузки и барьер хранения, которые представляют собой барьеры чтения и барьеры записи. Барьеры памяти выполняют две функции:
  • Предотвращение переупорядочивания инструкций по обе стороны барьера
  • Принудительно обновите данные в кеше или запишите их в основную память. Load Barrier отвечает за обновление кеша, а Store Barrier отвечает за запись содержимого кеша обратно в основную память.

LoadLoad, StoreStore, LoadStore, StoreLoad на самом деле являются комбинацией двух вышеупомянутых барьеров в Java для завершения ряда барьеров и функций синхронизации данных:
  • Барьер LoadLoad: Для такого оператора Load1, LoadLoad, Load2, до того, как данные, которые должны быть прочитаны Load2, и последующие операции чтения будут доступны, гарантируется, что данные, которые должны быть прочитаны Load1, были прочитаны.
  • Барьер StoreStore: для оператора Store1, StoreStore, Store2 операции записи Store1 гарантированно будут видны другим процессорам до выполнения Store2 и последующих операций записи.
  • Барьер LoadStore: для такого оператора Load1; LoadStore; Store2, прежде чем Store2 и последующие операции записи будут сброшены, убедитесь, что данные, которые должны быть прочитаны Load1, прочитаны.
  • Барьер StoreLoad: для оператора Store1; StoreLoad; Load2 записи Store1 гарантированно будут видны всем процессорам до выполнения Load2 и всех последующих операций чтения. Его накладные расходы - самый большой из четырех барьеров. В большинстве реализаций процессоров этот барьер является универсальным барьером, функционирующим как три других барьера памяти.



  • Барьер StoreStore гарантирует, что все нормальные записи были видны всем процессорам перед энергозависимой записью, а барьер StoreStore гарантирует, что все обычные записи были сброшены в основную память перед энергозависимой записью.
  • Барьер StoreLoad позволяет избежать переупорядочивания энергозависимых операций записи и последующих операций чтения/записи энергозависимых данных, которые могут возникнуть. Поскольку компилятор не может точно определить, нужно ли вставлять барьер StoreLoad после записи volatile (он возвращается сразу после записи, в это время нет необходимости добавлять барьер StoreLoad), для достижения правильной семантики памяти volatile , JVM использует консервативную стратегию. Добавляйте барьер StoreLoad после каждой записи или перед каждым чтением энергозависимой переменной. В большинстве сценариев один поток записывает энергозависимую переменную, а несколько потоков считывают ее. Количество потоков, считываемых одновременно, на самом деле намного больше, чем количество потоков. написано. Выбор добавления барьера StoreLoad после энергозависимой записи значительно повысит эффективность выполнения (накладные расходы на барьер StoreLoad упоминались выше).


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

Даже если JMM так много работает с volatile, он гарантирует правильную синхронизацию volatile-переменных между несколькими потоками при атомарных операциях, а для неатомарных операций использование volatile все равно приведет к непредсказуемым результатам.
Например, для операций i++ результат все еще не определен в случае многопоточности:
пример:



Давайте используем javap -c, чтобы увидеть инструкции по компиляции для этого файла:


В инструкции компиляции метода увеличения мы видим, что операция ++ прошла 4 шага:
1. getstatic #10 Получите статическую переменную num и поместите ее на вершину стека.На данный момент значение volatile-гарантии правильное.
2, константа типа 1 icont_1 int помещается в стек
3. iadd добавляет два значения int на вершину стека, и результат помещается на вершину стека.
4. putstatic #10 Поместите отрицательное значение вверху стека в указанное поле.
Проблема заключается в шагах 2 и 3. При выполнении этих двух шагов переменная volatile могла быть изменена другими потоками.

Согласно семантике памяти volatile, мы можем обобщить два способа безопасного использования volatile:
  • Результат операции не зависит от текущего значения volatile-переменной, либо гарантируется, что только один поток может модифицировать значение переменной
  • Переменные не нужно участвовать в инвариантсе с другими переменными государства.