Поговорите о JMM, чтобы ваше интервью стало вишенкой на торте

Java
在这里插入图片描述
вставьте сюда описание изображения

Ключевые проблемы параллельного программирования

JDKродившийсяЭто многопоточность Многопоточность значительно увеличивает скорость работы программы, но у всего есть свои преимущества и недостатки Параллельное программирование часто включает взаимодействие между потоками.通信и同步Обычно также говорят, что проблемой является видимость, атомарность и упорядоченность.

нить связи

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

  1. Общая память.

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

  1. Передача сообщений, например, в механизмах синхронизации системы Linux включает каналы, сигналы, очереди сообщений, семафоры и сокеты.

В параллельной модели передачи сообщений между потоками нет общего состояния, и потоки должны взаимодействовать явно, отправляя сообщения явно.Типичный метод связи в Java:wait()иnotify().

В C/C++ вы можете同时支持Общая память и механизм передачи сообщений, который используется в Java共享内存模型.

синхронизация потоков

Синхронизация — это когда программа используется для управленияразные темыМеханизм относительного порядка выполнения операций между ними.

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

JMM

Модель памяти современной компьютерной физики

тайник

Прежде чем понять JMM, давайте сначала разберемся с физической моделью хранения данных современных компьютеров.在这里插入图片描述С развитием технологии ЦП скорость выполнения ЦПбыстрее и быстрее. Поскольку технология памяти не сильно изменилась, то на выполнение задачи у меня ушло в общей сложности секунд 10. В итоге на получение данных ЦП ушло 8 секунд, на вычисление ЦП - 2 секунды, и большая часть время потрачено на получение данных.

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

После запуска программы для получения данных выполняются следующие действия.在这里插入图片描述И с постоянным улучшением вычислительной мощности ЦП один уровень кеша постепенно не может удовлетворить требования, и постепенно создается многоуровневый кеш. По порядку чтения данных и степени тесной интеграции с ЦП кэш ЦП можно разделить на кэш первого уровня.L1), кэш второго уровня (L2), некоторые высокопроизводительные процессоры также имеют кэш-память L3 (L3), все данные, хранящиеся в кэше каждого уровня, являются частью кэша следующего уровня. Техническая сложность и стоимость изготовления этих трех кэшей относительно снижаются, поэтому их емкость также относительно увеличивается.Сравнение производительности выглядит следующим образом:在这里插入图片描述

Одноядерный ЦП содержит только набор кэшей L1, L2 и L3; если ЦП содержит несколько ядер, то есть многоядерный ЦП, каждое ядро ​​содержит набор кэшей L1 (и даже L2), в то время как общие кэши L3 (или и L2).在这里插入图片描述

когерентность кэша

Благодаря постоянному повышению мощности компьютеров поддерживается многопоточность. Затем возникает проблема. Давайте проанализируем влияние однопоточности и многопоточности на одноядерный и многоядерный процессор соответственно.

  1. одиночная нить. Кэш ядра процессора доступен только одному потоку. Кэш эксклюзивный, и конфликтов доступа и прочих проблем не будет.
  2. Многопоточность одноядерного процессора. Несколько потоков в процессе будут получать доступ к общим данным в процессе одновременно.После того, как ЦП загружает определенный фрагмент памяти в кеш, разные потоки получают доступ к данным.такой жеКогда физический адрес , будет сопоставлен стот же кешположение, так что даже если произойдет переключение потока, кеш не станет недействительным. Но поскольку в любой момент времени может выполняться только один поток, нарушение доступа к кешу отсутствует.
  3. Многоядерный процессор, многопоточность. Каждое ядро ​​имеет как минимум один кэш L1. Если несколько потоков обращаются к общей памяти в процессе, и эти несколько потоков выполняются на разных ядрах, каждое ядро ​​будетcaeheБуфер разделяемой памяти зарезервирован в файле . Поскольку несколько ядер могут быть распараллелены, может появиться несколько потоков.Одновременно записывать в соответствующие кешиВ этом случае данные между соответствующими кэшами могут отличаться.

Добавление кеша между ЦП и основной памятью может вызвать проблемы когерентности кеша в многопоточных сценариях, то есть в многоядерном ЦП собственный кеш каждого ядра, примерно одни и те же данные缓存内容可能不一致.在这里插入图片描述 когерентность кэша(Согласованность кеша): в многопроцессорной системе каждый процессор имеет свой собственный кеш и использует одну и ту же основную память (MainMemory). Когда вычислительные задачи нескольких процессоров включают одну и ту же область основной памяти, соответствующие данные кэша могут быть несогласованными, например, переменная в общей памяти совместно используется несколькими процессорами. Если это произойдет, чьи кэшированные данные используются при синхронизации с основной памятью? Чтобы решить проблему когерентности, каждому процессору необходимо получить доступ к кешу.遵循一些协议, При чтении и записи необходимо действовать по протоколу, к таким протоколам относятся MSI, MESI (Illinois Protocol), MOSI, Synapse, Firefly и Dragon Protocol.

Несогласованная демонстрация выглядит следующим образом:

//线程A 执行如下
a = 1 // A1
x = b // A2
-----
// 线程B 执行如下
b = 2 // B1
y = a // B2

在这里插入图片描述Процессор A и процессор B выполняют доступ к памяти параллельно в программном порядке и могут получить результат x=y=0.

  1. Процессор A и процессор B могут одновременно записывать общие переменные в свои собственные буферы записи (A1, B1), a=1, b=2.
  2. Операции записи a = 1 и b = 2 не завершаются до тех пор, пока они не будут сброшены в общий кэш A3 и B3.
  3. Если этот шаг выполняется на втором шаге(A2, B2) выполняется, x=b, y=a. Программа может получить результат x=y=0.
Оптимизация процессора и изменение порядка инструкций

Как упоминалось выше, добавление кеша между ЦП и основной памятью вызовет проблемы когерентности кеша в многопоточных сценариях. В дополнение к этой ситуации, есть проблема с оборудованием, что также более важно. То есть сделать так, чтобы арифметический блок внутри процессора можно было использовать как можно больше.充分利用, процессор может выполнить неупорядоченное выполнение входного кода. Это оптимизация процессора.

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

Вполне возможно, что если оптимизация процессора и компилятор изменит порядок инструкций, это может вызвать всевозможные проблемы. И аппаратный уровень, и уровень компилятора решат эти проблемы.

Параллельные проблемы программирования

Упомянутые выше проблемы связаны с аппаратным обеспечением, нам нужно знать, что базовый уровень программного обеспечения является аппаратным, и программное обеспечение появится при работе на таком уровне.原子性,可见性,有序性проблема. На самом деле, проблемы атомарности, проблемы видимости и проблемы упорядочения. Это абстрактно определяется людьми. И этот рефератосновная проблемаЭто когерентность кэша, оптимизация процессора и проблемы с перестановкой команд, упомянутые ранее.

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

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

ты можешь найти缓存一致性Проблема в том, что可见性проблема. и处理器优化может привести к原子性сомнительный.指令重排это приведет к有序性проблема.

модель памяти

Как упоминалось ранее, проблемы когерентности кэш-памяти, оптимизации процессора и перераспределения команд вызваны постоянным обновлением аппаратного обеспечения. Итак, существует ли какой-либо механизм для решения вышеуказанных проблем? Чтобы обеспечить атомарность, видимость и упорядоченность в параллельном программировании. Существует важное понятие, т.内存模型.

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

Модель памяти решает проблему параллелизма главным образом двумя способами:限制处理器优化и использовать内存屏障.

JMM

Кстати о компьютерахмодель памятиЭто важная спецификация для решения проблем параллелизма в многопоточных сценариях. Так что же такое конкретная реализация?Разные языки программирования могут иметь разные реализации.

Мы знаем, что программы Java должны выполняться на виртуальной машине Java, а модель памяти Java (Java Memory Model, JMM) — это механизм и спецификация, которые соответствуют спецификации модели памяти, скрывают различия в доступе для различных аппаратных средств и операционных систем и обеспечивают доступ программ Java к памяти на различных платформах.

Когда речь заходит о модели памяти Java, обычно речь идет о новой модели памяти, используемой в JDK 5, которая в основном состоит изJSR-133: Описание модели памяти JavaTM и спецификации потоков. Простое изображение выглядит следующим образом:在这里插入图片描述Функция JMM:

Это фиктивная спецификация, которая действует на工作内存и主存之间Процесс синхронизации данных. Цель состоит в том, чтобы решить проблемы, вызванные несогласованностью данных в локальной памяти, изменением порядка инструкций кода компилятором и неправильным выполнением кода процессором, когда несколько потоков взаимодействуют через общую память.

PS:

Упомянутые здесь основная память и рабочая память (кэш, регистр) могут быть просто аналогичны понятиям основной памяти и кэша в модели памяти компьютера. Следует отметить, что основная память и рабочая память не являются тем же уровнем разделения памяти, что и куча Java, стек, область методов и т. д. в структуре памяти JVM, и не могут сравниваться напрямую. В «Глубоком понимании виртуальной машины Java» считается, что, если она должна едва соответствовать определению переменных, основной памяти и рабочей памяти, основная память в основном соответствует части данных экземпляра объекта в куче Java. Рабочая память соответствует части стека виртуальной машины.

Связь между произвольными потоками проста:在这里插入图片描述Внутри JVM модель памяти Java делит память на две части: область стека потоков и область кучи.Сообщение блога, здесь дана только общая схема архитектуры, а детали написаны.在这里插入图片描述

Проблемы с JMM
  1. Общий объект для каждого потока可见性

После того, как поток A прочитает данные основной памяти и модифицирует их, прежде чем он успеет синхронизировать измененные данные с основной памятью, данные основной памяти снова считываются потоком B.

  1. общий объект竞争Феномен

Два потока AB одновременно считывают данные основной памяти, затем одновременно добавляют 1, а затем возвращаются.

在这里插入图片描述Для вышеуказанной проблемы есть не что иное, как использование переменнойvolatile, замок,CASПодождите, пока это действие разрешится.

перестановка инструкций

При выполнении программ компиляторы и процессоры часто меняют порядок инструкций для повышения производительности. Например:

code1 // 耗时10秒
code2 // 耗时2秒
----
如果code1跟code2符合指令重拍的要求,code2不会一直等到code1执行完毕再执行。

Скомпилированный исходный код может быть ускорен следующими перестановками, чтобы он стал окончательными инструкциями, выполняемыми ЦП.在这里插入图片描述

  1. Переупорядочивание, оптимизированное для компилятора

Компилятор может изменить порядок выполнения операторов без изменения семантики однопоточной программы.

  1. Параллельное переупорядочивание на уровне инструкций

Современные процессоры используют параллелизм на уровне инструкций (ILP) для перекрытия выполнения нескольких инструкций. Если зависимостей данных нет, процессор может изменить порядок, в котором операторы соответствуют машинным инструкциям.

  1. Переупорядочивание системы памяти

Процессор использует кэши и буферы чтения/записи, из-за чего загрузка и сохранение выполняются не по порядку (переупорядочение процессора).

Зависимость данных и зависимость управления

Изменение порядка не переделывает код для зависимостей данных и зависимостей управления.

  1. зависимость данных Если две операции обращаютсята же переменная, и в этих двух операцияхЕсть запись操作,此时这两个操作之间就存在数据依赖性,这样的代码都不允许重排(重排后结果就不一样了)。 Существует три типа зависимостей данных:在这里插入图片描述
  2. управляющие зависимости Переменная флага — это флаг, используемый для определения того, была ли записана переменная a. В методе использования суждение о том, если (флаг), больше зависит от переменной i, чем от переменной i. Это называется зависимостью управления. Если происходит переупорядочение , результат будет неверным.
public void use(){
   if(flag){ //A
      int i = a*a;// B 
      ....
   }
}
as-if-serial

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

Как бы ни переупорядочивались (компилятор и процессор для увеличения параллелизма), результат выполнения (однопоточной) программы изменить нельзя. Компилятор, среда выполнения и процессор должны подчиняться семантике «как если бы — последовательная». В соответствии с семантикой «как если бы» компиляторы и процессоры не переупорядочивают операции, которые имеют зависимости от данных, потому что такое переупорядочивание может изменить результат выполнения. (Подчеркивается, что упомянутые здесь зависимости данных касаются только последовательности инструкций, выполняемых в одном процессоре, и операций, выполняемых в одном потоке, а зависимости данных между разными процессорами и между разными потоками не затрагиваются компилятором и процессором. .Учтите) Однако эти операции все же могут быть переупорядочены компилятором и процессором, если между ними нет зависимостей по данным.

int a = 1; //1
int b = 2;//2
int c = a + b ;// 3

Существует зависимость данных между 1 и 3, а также зависимость данных между 2 и 3. Таким образом, в окончательной выполненной последовательности инструкций 3 нельзя переставить перед 1 и 2 (3 перед 1 и 2, результат программы будет изменен). Но между 1 и 2 нет зависимости данных, компилятор и процессор могут изменить порядок выполнения между 1 и 2. семантика asif-serial избавляет от необходимости беспокоиться о переупорядочении помех и проблем с видимостью памяти в рамках одного потока.

Проблема переупорядочивания при многопоточности

Например, две классические функции в следующем классе, если поток AB одновременно выполняет разные функции,

  1. Поток A переставляет 12 инструкций, а порядок выполнения потоков AB — 2-3-4-1.
  2. Поток B переупорядочивает инструкции для 34, сначала считывает значение 0, затем вычисляет a*a = 0, временно сохраняет его, а затем, если поток A завершает выполнение, i в функции использования наконец равно 0.在这里插入图片描述

Решайте проблемы в режиме параллелизма

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

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

  1. Гарантирует порядок, в котором выполняются определенные операции.
  2. Влияет на видимость в памяти некоторых данных (или результата выполнения инструкции).

Компиляторы и ЦП могут переупорядочивать инструкции, чтобы гарантировать одинаковый конечный результат, пытаясь оптимизировать производительность. вставитьMemory Barrierсообщит компилятору и процессору:

  1. Никакая команда не может соответствовать этомуMemory BarrierПереупорядочивание инструкций.
  2. Memory BarrierДругая вещь, которую он делает, это заставляет различныеCPU cache, какWrite-Barrier(Барьер записи) удалит всеBarrierнаписано раньшеcacheданные, так что любой поток на ЦП может прочитать последнюю версию этих данных.

В настоящее время существует 4 барьера.

  1. НагрузкаГрузовой барьер

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

  1. StoreStore Барьер Запись Запись

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

  1. Барьер LoadStore Чтение Запись

Последовательность: Load1; LoadStore; Store2 Примерно аналогично первому, он гарантирует, что данные Load1 будут считаны до того, как Store2 и последующие инструкции Store будут сброшены. Барьеры LoadStore требуются для процессоров, вышедших из строя, ожидающих инструкций Store для передачи инструкций загрузки.

  1. Барьер StoreLoad Запись Чтение

Последовательность: Store1; StoreLoad; Load2 Перед загрузкой в ​​Load2 и всеми последующими инструкциями по загрузке убедитесь, что данные Store1 становятся видимыми для других процессоров (имея в виду сброс в память). Барьеры StoreLoad вызывают выполнение всех инструкций доступа к памяти (сохранение и загрузка) перед барьером до выполнения инструкций доступа к памяти после барьера. StoreLoad Barriers — это全能型, который имеет эффект трех других барьеров одновременно. Большинство современных мультипроцессоров поддерживают этот барьер (другие типы барьеров не обязательно поддерживаются всеми процессорами).

критическая секция

То есть блокировка, при запуске двух функций добавляется одинаковая блокировка, которая обеспечивает упорядоченное выполнение двух функций двумя потоками, и должна отвечать только за метод синхронизации.as-if-serialВот и все.在这里插入图片描述

Happens-Before

Поскольку перестановка инструкций затрудняет понимание внутренних правил работы ЦП, JDK используетhappens-beforeКонцепция видимости памяти между операциями. В JMM, если результат выполнения одной операции должен быть виден другой операции, между двумя операциями должно существоватьhappens-beforeотношение . где процессорhappens-beforeЭто гарантируется без каких-либо средств синхронизации.

  1. если операцияhappens-beforeдругой операции, результат первой операции будет виден второй операции, и первая операция будет выполнена перед второй операцией. (для программистов)
  2. существует между двумя операциямиhappens-beforeотношения, не означает, что конкретная реализация платформы Java должна выполняться в порядке, указанном отношением «происходит до». Если результат выполнения после переупорядочивания такой же, как при нажатииhappens-beforeЕсли результат тот же, переупорядочение разрешено (как для компилятора, так и для процессора)

在这里插入图片描述 happens-beforeКонкретные правила отмечены под отметкой на случай чрезвычайной ситуации.

  1. Правила порядка выполнения программы: каждая операция в потоке выполняется до любых последующих операций в этом потоке.
  2. Следите за правилами блокировки: отпирание замка происходит до последующего запирания замка.
  3. Правило изменчивой переменной: запись в изменчивое поле происходит до любого последующего чтения изменчивого поля.
  4. Переходный: если А происходит раньше В, а В происходит раньше С, то А происходит раньше С.
  5. Правило start(): если поток A выполняет операцию ThreadB.start() (запуская поток B), то операция ThreadB.start() потока A происходит до любой операции в потоке B.
  6. Правило join(): если поток A выполняет операцию ThreadB.join() и завершается успешно, то любая операция в потоке B происходит до того, как поток A успешно завершится из операции ThreadB.join(). 7. Правила прерывания потока: вызов метода прерывания потока происходит до того, как код прерванного потока обнаружит возникновение события прерывания.
изменчивая семантика

volatileГарантированная видимость переменных, а также слабая атомарность. оvolatileДетали, написанные в предыдущих сообщениях в блоге, не будут повторяться, когда инструкции переставлены, они верны.volatileПравила следующие:

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

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

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

Компиляторы и процессоры подчиняются двум правилам переупорядочения.

  1. Нет никакого переупорядочения между записью конечного поля в конструкторе и последующим присвоением ссылки на созданный объект ссылочной переменной. см. кодовое примечание 1
  2. Нет никакого переупорядочения между начальным чтением ссылки на объект, содержащий конечное поле, и последующим начальным чтением конечного поля. см. кодовое примечание 2
class SoWhat{
    final int b;
    SoWhat(){
        b = 1412;
    }
    public static void main(String[] args) {
        SoWhat soWhat = new SoWhat();
        // 备注1:禁止在 b = 1 这个语句执行完之前,系统将新new出来的对象地址赋值给了sowhat。
        System.out.println(soWhat); //A
        System.out.println(soWhat.b); //B
        // 备注2: A  B 两个指令不能重排序。
    }
}

Когда final является ссылочным типом, добавляются следующие правила:

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

class SoWhat{
    final Object b;
    SoWhat(){
        this.b = new Object(); //  A
    }
    public static void main(String[] args) {
        SoWhat soWhat = new SoWhat(); //B
        // 含义是 必须A执行完毕了 才可以执行B
    }
}

Реализация финальной семантики в процессорах

  1. Компилятор должен вставить барьер StoreStore после записи последнего поля и до возврата конструктора.
  2. Правила переупорядочивания для чтения конечных полей требуют, чтобы компилятор вставил барьер LoadLoad перед операциями, считывающими конечные поля.
synchronized

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

Ссылаться на

Схема структуры памяти компьютера Довольно неплохая модель компьютерной памяти

В этой статье используетсяmdniceнабор текста