Тщательно понять изменчивый

Java задняя часть программист переводчик
Тщательно понять изменчивый

Оригинальная статья, краткое изложение опыта и жизненные перипетии на всем пути от набора в школу до фабрики А

Нажмите, чтобы узнать подробностиwww.codercc.com

1. Введение в volatile

В предыдущей статье мы подробно разобрались с ключевыми словами Java.synchronized, мы знаем, что в java есть еще один великий артефакт, который является ключевым volatile, который, можно сказать, является лидером в синхронизации с синхронизированным.Давайте обсудим тайну вместе.

Из предыдущей статьи мы узнали, что synchronized — это блокирующая синхронизация, и в случае интенсивной конкуренции потоков она будет повышена до тяжеловесной блокировки. И можно сказать, что volatile является самым легким механизмом синхронизации, предоставляемым виртуальной машиной Java. Но в то же время правильно понять это непросто, и многие программисты используют синхронизацию, когда сталкиваются с проблемами безопасности потоков при параллельном программировании.Модель памяти JavaЭто говорит нам о том, что каждый поток будет копировать общую переменную из основной памяти в рабочую память, а затем исполнительный механизм будет работать на основе данных в рабочей памяти. Когда поток записывает в основную память после выполнения операции в рабочей памяти? Для обычных переменных этот тайминг не указан, но для volatile-модифицируемых переменных виртуальная машина Java имеет специальное соглашение, изменение потоком volatile-переменных будет немедленно воспринято другими потоками, то есть не будет грязного чтения данных , тем самым обеспечивая "Видимость" данных.

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

2. Принцип энергозависимой реализации

Как реализована volatile? Например, очень простой код Java:

instance = new Instance() //экземпляр - это изменчивая переменная

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

  1. Записать данные текущей строки кэша процессора обратно в системную память;
  2. Эта операция обратной записи сделает недействительными данные, кэшированные по адресу памяти в других процессорах.

В целях повышения скорости обработки процессор не связывается напрямую с памятью, а сначала считывает данные из системной памяти во внутренний кэш (L1, L2 или другой) перед выполнением операции, но не знает, когда это произойдет. быть записаны в память после операции. Если операция записи выполняется для переменной, объявленной как volatile, JVM отправит процессору инструкцию с префиксом Lock для записи данных строки кэша, в которой находится переменная, обратно в системную память. Однако, даже если оно будет записано обратно в память, если значение, закэшированное другим процессором, все еще старое, повторно выполнить операцию вычисления будет проблематично. Поэтому в мультипроцессорах для обеспечения согласованности кешей каждого процессора будет реализованокогерентность кэшапротокол,Каждый процессор проверяет, не устарели ли его кэшированные значения, перехватывая данные, распространяющиеся по шине.Теперь, когда процессор обнаружит, что адрес памяти, соответствующий его строке кэша, был изменен, он установит текущую строку кэша процессора в недопустимое состояние.Когда процессор изменяет данные, он перезагрузит данные из системной памяти.Чтение в кэш процессора. Таким образом, проведя анализ, мы можем сделать следующие выводы:

  1. Инструкции с префиксом блокировки вызывают запись кэша процессора обратно в память;
  2. Обратная запись кэша одного процессора в память может привести к тому, что кэши других процессоров станут недействительными;
  3. Когда процессор обнаружит, что локальный кеш недействителен, он перечитает данные переменной из памяти, то есть может быть получено текущее самое последнее значение.

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

3. Бывает-до отношений летучих

После вышеприведенного анализа мы уже знаем, что volatile-переменные можно передавать черезПротокол когерентности кэшаГарантируется, что каждый поток может получить самое последнее значение, то есть "видимость" данных будет удовлетворена. Продолжаем путь анализа проблем из предыдущей статьи (я всегда считаю, что способ мышления о проблемах принадлежит мне, и он самый важный, и я также постоянно культивирую способности в этой области).Я всегда разделял точки входа параллельного анализа вДва ядра, три свойства. Два ядра: модель памяти JMM (основная память и рабочая память) и «происходит до»; три свойства: атомарность, видимость и упорядоченность (краткое изложение трех свойств будет обсуждаться с вами в следующих статьях). Без дальнейших церемоний, давайте сначала рассмотрим одно из двух основных: отношение «происходит до» volatile.

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

public class VolatileExample {
    private int a = 0;
    private volatile boolean flag = false;
    public void writer(){
        a = 1;          //1
        flag = true;   //2
    }
    public void reader(){
        if(flag){      //3
            int i = a; //4
        }
    }
}

Отношение «происходит до», соответствующее приведенному выше примеру кода, показано на следующем рисунке:

VolatileExample的happens-before关系推导

Блокирующий поток A сначала выполняет метод записи, а затем поток B выполняет метод чтения.Каждая стрелка и два узла на рисунке кодируют отношение «происходит до», а черные представляют в соответствии справила порядка программыВывод, красный основан наЗапись изменчивой переменной происходит до любого последующего чтения изменчивой переменной., а синие получены из транзитивного правила. Здесь 2 происходит раньше, чем 3 также определяется в соответствии с правилом: если А происходит раньше В, то результат выполнения А виден В, а порядок выполнения А предшествует порядку выполнения В, мы можем знать, что выполняется операция 2. Результат виден операции 3, а это означает, что поток B может быстро обнаружить, когда поток A изменяет флаг volatile переменной на true.

4. Семантика памяти volatile

или согласнодва ядраПосле анализа отношения «происходит — прежде» мы теперь дополнительно проанализируем семантику памяти volatile (обучение таким образом позволит каждому получить более глубокое понимание знаний, не будучи перегруженным? Если вы согласны с моим методом, вы могли бы также дать например, мой брат благодарит меня здесь, это ободряет меня). Взяв приведенный выше код в качестве примера, предположим, что поток A сначала выполняет метод записи, а поток B выполняет метод чтения позже.В начале флаг и a в локальной памяти потока являются начальными состояниями.На следующем рисунке показано состояние диаграмма потока A после выполнения энергозависимой записи.

线程A执行volatile写后的内存状态图

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

线程B读volatile后的内存状态图

С горизонтальной точки зрения существует связь между потоком A и потоком B. Когда поток A записывает изменчивую переменную, это на самом деле похоже на отправку сообщения потоку B, чтобы сообщить потоку B, что ваше текущее значение устарело, а затем Когда поток B читает эту изменчивую переменную, это похоже на получение сообщения, только что отправленного потоком A. Поскольку он старый, что мне делать с потоком B? Естественно, его можно получить только из основной памяти.

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

4.1 Реализация семантики энергозависимой памяти

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

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

Барьеры памяти JMM делятся на четыре категории, как показано на рисунке ниже.

内存屏障分类表

Компилятор Java вставляет инструкции барьера памяти в соответствующие места при создании последовательностей инструкций, чтобы запретить определенные типы переупорядочения процессора. Чтобы достичь семантики памяти volatile, JMM ограничит определенные типы переупорядочения компилятора и процессора, а JMM сформулирует таблицу правил переупорядочения volatile для компилятора:

volatile重排序规则表

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

  1. при каждой энергозависимой операции записиФронтВставьте барьер StoreStore;
  2. при каждой энергозависимой операции записипозжеВставьте барьер StoreLoad;
  3. при каждом изменчивом чтениипозжеВставьте барьер LoadLoad;
  4. при каждом изменчивом чтениипозжеВставьте барьер LoadStore.

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

МагазинМагазин Барьер: Запретить нормальную запись выше и изменение порядка энергозависимой записи ниже;

Барьер StoreLoad: предотвратить переупорядочивание энергозависимых записей выше с возможными энергозависимыми операциями чтения/записи ниже

НагрузкаГрузовой барьер: Запретить все обычные операции чтения ниже и переупорядочивание энергозависимого чтения выше.

Барьер LoadStore: Запрещает все обычные операции записи, описанные ниже, и изменение порядка чтения, описанное выше.

Для понимания используются следующие две схемы, рисунки взяты из очень хорошей книги "The Art of Java Concurrent Programming".

volatile写插入内存屏障示意图

volatile读插入内存屏障示意图

5. Пример

Теперь мы понимаем суть volatile, и я думаю, мы все можем ответить на вопрос, поставленный в начале статьи. Исправленный код:

public class VolatileDemo {
    private static volatile boolean isOver = false;

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isOver) ;
            }
        });
        thread.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isOver = true;
    }
}

Обратите внимание на разницу, это сейчасУстановите isOver в изменчивую переменную, так что после изменения isOver на true в основном потоке значение переменной рабочей памяти потока будет недопустимым, поэтому значение нужно снова прочитать из основной памяти, и теперь можно прочитать последнее значение isOver, чтобы быть true для завершения потока. В нем есть бесконечный цикл, так что поток потока можно плавно остановить. Теперь проблема решена и знания усвоены :). (Если вы считаете, что это неплохо, пожалуйста, поставьте лайк, это воодушевляет меня.)

использованная литература

Искусство параллельного программирования на Java