Подробное объяснение основного принципа изменчивости

Java

Сегодня мы поговорим об основном принципе volatile;

Спецификация языка Java определяет volatile следующим образом:

Язык программирования Java позволяет потокам получать доступ к общим переменным, чтобы убедиться, что общие переменные могут быть обновлены точно, а согласованность, потоки должны убедиться, что эта переменная получается в одиночку через строку.

Прежде всего, давайте начнем с определения.Официальное определение немного многословно. С точки зрения непрофессионала этоПоле изменяется с помощью volatile.Модель памяти Java гарантирует, что все потоки видят одно и то же значение переменной.Но это не гарантирует атомарную работу многопоточности. Это называется видимостью потока.Нам нужно знать, что он не может гарантировать атомарность.

Понятия, связанные с моделью памяти

Связь между потоками Java регулируется моделью памяти Java (JMM), которая определяет, когда изменения общих переменных одного потока видны другому потоку. JMM определяет абстрактную связь между потоками и основной памятью: переменные между потоками хранятся в основной памяти (основной памяти), и каждый поток имеет частную локальную память (локальную память), в которой хранятся копии общих переменных. Локальная память является абстракцией JMM и на самом деле не существует.


Если поток A взаимодействует с потоком B:

  1. Поток A должен сначала сбросить обновленные общие переменные из локальной памяти A в основную память.

  2. Поток B считывает обновленную общую переменную потока A из основной памяти.

Когда компьютер запускает программу, каждая инструкция выполняется в ЦП, а чтение и запись данных обязательно участвуют в процессе выполнения. Мы знаем, что данные, которые запускает программа, хранятся в основной памяти. В это время будет проблема. Чтение и запись данных в основную память не так быстры, как выполнение инструкций в ЦП. Если какое-либо взаимодействие необходимо Разберитесь с основной памятью, это сильно повлияет на эффективность, поэтому есть кеш процессора. Кэши ЦП уникальны для определенного ЦП и относятся только к потокам, работающим на этом ЦП.

С кешем ЦП, хотя проблема эффективности решена, он принесет новую проблему: непротиворечивость данных.Когда программа запущена, копия данных, необходимых для операции, будет скопирована в кеш ЦП.При выполнении операции ЦП больше не будет иметь дело с основной памятью, а будет напрямую читать и записывать данные из кеша. сбросит данные в основную память.

Например:

i++;

Когда поток выполняет эту строку кода, он сначала считывает i из основной памяти, затем копирует копию в кэш ЦП, затем ЦП выполняет операцию +1, а затем записывает данные +1 в кэш, последний шаг сбрасывается в основную память. С однопоточностью проблем нет, но есть проблема с многопоточностью.

А именно: если два потока A и B оба выполняют эту операцию (i++), согласно нашему обычному логическому мышлению, значение i в оперативной памяти должно быть равно 3, но так ли это?

проанализируйте, как показано ниже:

Два потока прочитают значение I (1) из основной памяти в их соответствующие кэши, затем поток A выполняет операцию +1 и записывает результат к кэше, и, наконец, пишет в основную память, в это время основная память я == 2, нить B делает ту же операцию, я все еще = 2 в основной памяти. Таким образом, окончательный результат 2 не 3. Это явление представляет собой проблему когерентности кэша.

Есть два решения для решения когерентности кэм:

  1. Добавляя LOCK# к шине;

  2. через протокол когерентности кэша.

Но есть проблема с вариантом 1,Он реализован монопольно, то есть, если шина заблокирована с помощью LOCK#, может работать только один ЦП, а остальные ЦП должны быть заблокированы, что неэффективно.

Второй вариант,Cache Coherence Protocol (протокол MESI) Обеспечивает согласованность копий общих переменных, используемых в каждом кэше.Таким образом, JMM решает эту проблему.

изменчивый принцип реализации

Когда записывается переменная общего доступа, модифицированная volatile, будет дополнительная инструкция префикса Lock, которая вызовет две вещи под многоядерным процессором.

  1. Сбросьте данные текущей строки кэша процессора в основную память системы.

  2. Этот сброс обратно в основную память делает недействительными данные, кэшированные другими ЦП по этому адресу общей переменной памяти.

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

сцены, которые будут использоваться

Volatile часто используется в двух сценариях: маркировка состояния, двойная проверка

  1. флаг состояния
//线程1
boolean stop = false;
while(!stop){
   doSomething();
}

//线程2
stop = true;

Этот код является типичным фрагментом кода, и многие люди могут использовать этот метод маркировки при прерывании потока. Но на самом деле, будет ли этот код работать правильно? Ветка будет прервана? Не обязательно, может быть, в большинстве случаев этот код может прервать поток, но он также может привести к прерыванию потока (хотя такая вероятность очень мала, пока это происходит, это вызовет бесконечный цикл).

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

Затем, когда поток 2 меняет значение стоповой переменной, но не успел записать в основную память, поток 2 переключается на другие дела, тогда поток 1 продолжит цикл, потому что поток 2 не знает об изменении стопа переменная по потоку 2 идет вниз.

А вот с volatile проблем нет.Следующим образом:

    volatile boolean flag = false;

    while(!flag){
       doSomething();
    }

    public void setFlag() {
       flag = true;
    }

    volatile boolean inited = false;
    //线程1:
    context = loadContext();  
    inited = true;            

    //线程2:
    while(!inited ){
    sleep()
    }
    doSomethingwithconfig(context);
  1. double check
public class Singleton{
   private volatile static Singleton instance = null;

   private Singleton() {

  }

   public static Singleton getInstance() {
       if(instance==null) {
           synchronized (Singleton.class) {
               if(instance==null)
                   instance = new Singleton();
          }
      }
       return instance;
  }
}

Гость офицер считает полезным попросить или собрать, обратите внимание на номер паблика Javastorm, найдете интересную душу!
Позже мы продолжим анализ связанных технологий модели памяти JMM.

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

JavaStorm.png