Модель памяти в Java Concurrency

Java

Что такое JavaMemoryModel (JMM)?

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

ЦП и память

Прежде чем говорить о JMM, я хочу поговорить с вами об аппаратном уровне. Всем следует знать, что сам ЦП не имеет емкости для выполнения операций, он отвечает только за выполнение соответствующих операций над данными, переданными в соответствии с инструкциями, а задача хранения данных передается памяти для выполнения. Хотя скорость работы памяти намного выше, чем у жесткого диска, она все же слишком медленная по сравнению с процессором 3 ГГц, 4 ГГц или даже 5 ГГц.В глазах процессора скорость работы памяти просто младший брат младшего брата Подождите, пока память запустится один раз Для операций чтения и записи процессор может думать сотни тысяч жизней 😁. Однако вычислительная мощность ЦП — дефицитный ресурс, и его нельзя тратить попусту, поэтому приходится искать способ решения этой проблемы.

Нет проблем, которые не может решить кеш.Если есть, добавьте еще один кеш - Лу Синь: Я все равно этого не говорил.

Поэтому люди подумали о добавлении кеша в ЦП (почему добавление кеша вместо увеличения скорости памяти связано с проблемой стоимости оборудования), чтобы решить эту проблему, например, процессор Intel I9 9900k, используемый блогерами. , поэтому модель памяти на оборудовании примерно такая, как показано на рисунке ниже.

Как видно из рисунка, внутри ЦП встроены один или несколько уровней кэшей, причем кэш L1 уникален для ядра в ЦП и не может использоваться совместно между разными ядрами, в то время как кэш L2 полностью разделяет ядра. . Проще говоря, когда ЦП считывает фрагмент данных, он сначала переходит на ближайший уровень кэша, чтобы прочитать его. Если он не может его найти, он переходит на следующий уровень, чтобы найти его. это, он загрузит его из памяти.Если вы можете получить нужные вам данные, вы не будете обращаться к памяти, чтобы прочитать их. При обратной записи данных они тоже сначала будут записываться в Кэш, а потом уже после ожидания нужного времени писать в память (одна из деталей — проблема строк кеша, которая в конце статьи). Поскольку существует несколько уровней кеша, а некоторые кеши нельзя использовать совместно, возникнут проблемы с видимостью памяти.

Возьмем простой пример: Предположим, что есть два ядра, CoreA и CoreB, и у них обоих есть собственный L1Chace и общий L2Cache. В то же время есть переменная X со значением 1, которая была загружена на L2Cahce. В это время CoreA должен использовать переменную X для выполнения операции.Сначала перейдите в L1Cache, чтобы найти ее, если она промахнулась, продолжайте искать ее в L2Cache, если она сработает успешно, загрузите X=1 в L1Cahce, а затем после ряд операций, измените X на 2 и запишите его в L1Cache. При этом CoreB для выполнения вычислений достаточно X. В это время он переходит в свой L1Cache для поиска, если промахивается, продолжает поиск в L2Cache, если попадание удачное, загружает X=1 в свой собственный кэш L1. На данный момент возникает проблема: CoreA явно изменил значение X на 2, но CoreB по-прежнему читает X=1, что является проблемой видимости памяти.

Увидев здесь маленьких друзей, возможно, захочется спросить, как у вас дела, блогер, вы постепенно забываете название того, что написали, и после того, как вы сказали модель памяти Java, что вы делаете с таким количеством аппаратных проблем? (╯‵□′)╯︵┻━┻

Основная память и рабочая память в Java

Не волнуйтесь, друзья, на самом деле JMM очень похож на модель выше на аппаратном уровне Если вы мне не верите, посмотрите на картинку ниже.

Как, не похоже, можно просто понять, так как рабочая память — это CPU в эксклюзивном потоке L1Cahce ядра, а основная память — это разделяемый L2Cache. Таким образом, вышеупомянутая проблема когерентности памяти также существует в JMM, в то время как JMM потребуется ряд правил для обеспечения согласованности памяти, что является сложной точкой многопоточного параллелизма Java, тогда JMM разработала какие правила?

##Взаимодействие между памятью Во-первых, это протокол взаимодействия между оперативной и оперативной памятью, в частности определены следующие операции (и эти операции гарантированно атомарны):

  • lock (блокировка) переменная, действующая на основную память, помечающая переменную как эксклюзивную для потока
  • Разблокировать (разблокировать) переменную, которая воздействует на основную память, освобождает переменную, находящуюся в заблокированном состоянии, и может быть заблокирована другими потоками только после освобождения
  • read (чтение) действует на переменную в основной памяти, и передает значение переменной из основной памяти в рабочую память потока, что удобно для последующих операций загрузки.
  • Load (загрузка) действует на переменную в рабочей памяти, которая помещает значение переменной, полученное операцией чтения из основной памяти, в копию переменной в рабочей памяти.
  • использовать (использовать) переменную, воздействующую на рабочую память, передавая значение переменной в рабочей памяти механизму выполнения, который будет выполняться каждый раз, когда виртуальная машина встречает инструкцию байт-кода, которой необходимо использовать значение переменной.
  • Assign (назначение) действует на переменные в рабочей памяти, присваивая значение, полученное от исполняющего движка, переменной в рабочей памяти, эта операция выполняется всякий раз, когда виртуальная машина встречает инструкцию байт-кода, которая присваивает значение переменной.
  • Store (хранение) воздействует на переменные в рабочей памяти и передает значение переменной из рабочей памяти в основную память для последующих операций записи.
  • write (запись) действует на переменную в основной памяти, она передает операцию сохранения из значения переменной в рабочей памяти в переменную в основной памяти.

Он также предусматривает, что при выполнении вышеуказанных восьми операций необходимо соблюдать следующие правила:

  • Если вы хотите скопировать переменную из основной памяти в рабочую память, вам нужно выполнить операции чтения и загрузки по порядку.Если вы синхронизируете переменную из рабочей памяти обратно в основную память, вам нужно выполнить операцию сохранения и записи операции последовательно. Но модель памяти Java требует только последовательного выполнения вышеуказанных операций, и нет никакой гарантии, что они должны выполняться последовательно.
  • Одна из операций чтения и загрузки, сохранения и записи не может появляться одна
  • Поток не может отменить свою последнюю операцию присваивания, т. е. переменная должна быть синхронизирована с основной памятью после того, как она была изменена в рабочей памяти.
  • Потоку не разрешено синхронизировать данные из рабочей памяти обратно в основную память без какой-либо причины (операция присваивания не выполнялась).
  • Новую переменную можно создать только в основной памяти, прямое использование неинициализированной (загрузить или присвоить) переменной в рабочей памяти не допускается. То есть перед реализацией операций использования и сохранения для переменной сначала должны быть выполнены операции назначения и загрузки.
  • Переменная может быть заблокирована только одним потоком одновременно, но операция блокировки может повторяться одним и тем же потоком несколько раз.После многократного выполнения блокировки переменная будет разблокирована только после такого же количества операций разблокировки. выполнено. блокировка и разблокировка должны быть сопряжены
  • Если операция блокировки выполняется над переменной, значение переменной в рабочей памяти будет очищено.Прежде чем исполнительный механизм использует переменную, необходимо повторно выполнить операцию загрузки или назначения, чтобы инициализировать значение переменной.
  • Если переменная ранее не была заблокирована операцией блокировки, ей не разрешается выполнять операцию разблокировки; также не разрешается разблокировать переменную, заблокированную другими потоками.
  • Перед выполнением операции разблокировки переменной переменная должна быть синхронизирована с оперативной памятью (выполняются операции сохранения и записи).

(Вышеупомянутая часть ссылается на содержание статьи «Углубленное понимание виртуальной машины Java») и цитирует ее.

volatile (включает видимость памяти и запрещает переупорядочивание инструкций)

Для переменных, изменяемых volatile, в JMM есть специальные условия.

видимость памяти

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

изменение порядка инструкций

Переупорядочивание инструкций — это операция, которую выполняет большинство ЦП, а JVM также имеет операции переупорядочивания инструкций во время выполнения. Просто поставьте 🌰

    private void test(){
        int a,b,c;//1
        a=1;//2
        b=3;//3
        c=a+b;//4
    }

Предположим, что есть такой метод выше, есть четыре внутренних строки кода. JVM может быть переупорядочиванием инструкций, а переупорядочивание директивы является как-будто-последовательным, независимо от того, как переупорядочиваются (компиляторы и процессоры для повышения степени параллелизма), результаты (однопоточной) программы не могут быть изменены. В соответствии с этим положением компилятор и процессор не переупорядочивают инструкции, имеющие зависимости, но никакие инструкции зависимостей не могут быть переупорядочены. В приведенном выше примере первая строка и строка 2,3,4 зависят, поэтому первая строка кода инструкции должна быть на 2,3,4 вперед, это невозможно для неопределенного назначения переменной. А между второй и третьей строками кода и без взаимозависимостей можно указать, где происходит переупорядочение, выполнить сначала 3, затем выполнить 2. Последняя строка кода 4 и 3 предыдущие строки кода имеют зависимости, поэтому он будет на окончательном выполнении.

Поскольку JVM специально указывает, что переупорядочивание инструкций имеет тот же эффект, что и несортировка, только в одном потоке, означает ли это, что при многопоточности возникнут проблемы? Ответ — да, переупорядочивание инструкций при многопоточности может привести к неожиданным результатам.

    int a=0;
    //flag作为一个标识符,标识是否写入完成
    boolean flag = false;
    public void writer(){
        a=10;//1
        flag=true;//2
    }
    public void reader(){
        if (flag)
            System.out.println("a:"+a);
    }

Предполагая, что существует класс, который имеет поля и методы из приведенных выше разделов, класс предназначен для использования флага в качестве флага, чтобы указать, завершена ли запись, что не будет проблемой в рамках одного потока. В настоящее время существует два потока, выполняющих методы записи и чтения соответственно. Проблема видимости памяти пока не рассматривается. Предполагается, что запись флага и сразу известна другим потокам. все думают, что значение выхода a равно Сколько? 10?

Даже без учета видимости памяти значение a может все еще выводить 0 в это время, что является проблемой, вызванной переупорядочением инструкций. Код в комментариях 1 и 2 в приведенном выше коде не имеет зависимостей.Нет проблем с выполнением сначала 1 или 2 в одном потоке.Согласно принципу как-будто-последовательно, в это время может произойти переупорядочение инструкций.

Ключевое слово volatile может запрещать переупорядочивание команд.

длинная, двойная проблема

Все мы знаем, что операции между основной памятью 8 и рабочей памятью, определенные JMM, являются атомарными, но есть некоторые исключения для двух 64-битных типов данных long и double.