Что такое volatile и на что следует обратить внимание при его использовании?
Связь между volatile и атомарностью, видимостью и упорядоченностью
Принцип реализации volatile (барьер памяти, кэш ЦП, протокол МЭСИ)
Разница между volatile и синхронизированным
1. Что такое изменчивый?
Летучий по-китайски означает изменчивый, неустойчивый. Это также ключевое слово в Java, используемое для изменения переменных.
В JMM (Java Memory Model, Java Memory Model) есть основная память, и у каждого потока тоже есть своя память (например, регистры). Для повышения производительности поток хранит копию переменной, к которой нужно получить доступ, в своей собственной памяти.
Таким образом, значение одной и той же переменной в памяти одного потока может не соответствовать значению в памяти другого потока или значению в основной памяти в определенный момент.
Переменная, объявленная как volatile, означает, что переменная будет изменена другими потоками в любое время.Каждый раз, когда поток использует переменную, он будет считывать последнее значение измененной переменной..
Схема модели памяти Java:
Примечание при использовании volatile:
volatileлибо ретушироватьпеременная экземпляравсе ещестатическая переменная, все нужно поместить в数据类型перед ключевым словом, т.е.String,intперед ожиданием.
volatileиfinalВы не можете изменить переменную одновременно. Volatile гарантирует, что при записи переменной результат будет виден другим потокам, а final сделает переменную невозможной для повторной записи.
2. Связь между volatile и атомарностью, видимостью и упорядоченностью
Введение в атомарность, видимость и упорядочение было представлено в предыдущей статье.портал
2.1 Может ли volatile гарантировать атомарность?
не может.
Мы знаем, что атомарность означает, что операцию больше нельзя разделить на несколько шагов. Операция или несколько операций выполняются либо все, и процесс выполнения не прерывается никаким фактором, либо ни одна из них не выполняется.
Для volatile, если сама операция не является атомарной, то использование volatile для воздействия на переменную в этой операции не может гарантировать атомарность.
Например, проблема i++, с которой мы часто сталкиваемся.
i = 1; //原子性操作,不用使用volatile也不会出现线程安全问题。
volatile int i = 0;
i++; //非原子性操作
Если мы запустим 200 потоков для одновременного выполненияi++Эта строка кода выполняется только один раз в каждом потоке. Если volatile может гарантировать атомарность, то окончательный результат i должен быть 200, на самом деле мы находим, что это значение будет меньше 200, в чем причина?
// i++ 其可以被拆解为
1、线程读取i
2、temp = i + 1
3、i = temp
Например, когда i=5, два потока A и B одновременно считывают значение i.
Затем поток A выполняетtemp = i + 1операции, следует отметить, что значение i в это время не изменилось, а затем поток B также выполнялсяtemp = i + 1Обратите внимание, что в это время значение i, сохраненное двумя потоками A и B, равно 5, а значение temp равно 6.
Затем поток A выполняетi = temp(6) операция, в это время значение i будет немедленно обновлено в основную память и уведомит другие потоки о том, что сохраненное значение i является недопустимым.В это время поток B должен повторно прочитать значение i, тогда i, сохраненный потоком B в это время, равен 6
В то же время темп, сохраненный потоком B, по-прежнему равен 6, а затем поток B выполняетi=temp(6), поэтому расчетный результат на 1 меньше ожидаемого.
Используйте классы, поддерживающие атомарные операции, такие какjava.util.concurrent.atomic.AtomicInteger, который использует алгоритм CAS (сравнить и поменять местами, сравнить и заменить), который более эффективен, чем первый.
2.2 изменчивость и видимость
Когда записывается переменная ключевого слова volatile, кеш принудительно синхронизируется с основной памятью.Когда другие потоки читают кеш и обнаруживают, что кеш недействителен, они будут считывать основную память, тем самым обеспечивая видимость переменной.
2.3 изменчивый и упорядоченный
Volatile может запретить переупорядочивание инструкций, поэтому он может гарантировать порядок.
Что такое изменение порядка инструкций?
В модели памяти Java компилятору и процессору разрешено переупорядочивать инструкции, не влияя на результаты переупорядочивания.один потокреализация, ноНевозможно гарантировать одновременное выполнение нескольких потоковвремя не влияет.
Например, если следующий код не переупорядочен, порядок его выполнения будет 1->2->3->4. Но в реальном исполнении это может быть 1->2->4->3 или 2->1->3->4 или другое. Но это гарантирует, что 1 предшествует 3, а 2 предшествует 4. Все конечные результатыa=10; b=20.
int a = 0;//语句1
int b = 1;//语句2
a = 10; //语句3
b = 20; //语句4
Но если это многопоточный случай, в другом потоке есть следующая программа. Когда указанный выше порядок выполнения изменяется на 1->2->4->3, когда поток 1 выполняется до шага 3b=20Когда переключитесь на выполнение потока 2, он выведетa此时已经是10了, а значение a все еще равно 0 в это время.
if(b == 20){
System.out.print("a此时已经是10了");
}
3. Принцип реализации изменчивого
3.1 Барьеры памяти и переупорядочивание инструкций
Чтобы узнать, как volatile запрещает переупорядочивание инструкций, вам сначала нужно понять концепцию内存屏障.
Барьер памяти (английский: Барьер памяти), также известный как барьер памяти, барьер памяти, инструкция барьера и т. д., является инструкцией ЦП, поэтому такие языки, как Java, C++ и C, имеют эту концепцию.
Это заставляет ЦП или компилятор работать в строгом соответствии с определенным порядком при работе с памятью, то есть инструкции до барьера памяти и инструкции после барьера памяти не будут нарушены из-за оптимизации системы и другие причины.
Барьер памяти, также известный как инструкция membar, memory guard или block, представляет собой тип инструкции барьера, который заставляет ЦП или компилятор применять ограничение порядка операций с памятью, выполняемых до и после инструкции барьера.Обычно это означает, что операции, выполненные до барьера гарантированно будут выполняться до операций, выполненных после барьера.
3.1.1 4 барьера памяти в JVM
Нагрузочный барьер:
//抽象场景:
Load1;
LoadLoad;
Load2
Load1 и Load2 представляют собой две инструкции чтения. Перед обращением к данным, которые должны быть прочитаны Load2, убедитесь, что данные, которые должны быть прочитаны Load1, были прочитаны.
Барьер магазина:
//抽象场景:
Store1;
StoreStore;
Store2
Store1 и Store2 представляют собой две инструкции записи. Перед выполнением записи Store2 убедитесь, что операции записи Store1 видны другим процессорам.
Барьер LoadStore:
//抽象场景:
Load1;
LoadStore;
Store2
Перед записью Store2 убедитесь, что данные, которые должны быть прочитаны Load1, были прочитаны.
Барьер StoreLoad:
//抽象场景:
Store1;
StoreLoad;
Load2
Запись в Store1 гарантированно будет видна всем процессорам до чтения Load2. Накладные расходы барьера StoreLoad — самый большой из четырех барьеров.
3.1.2 Взаимосвязь между volatile и барьерами памяти
Когда переменная становится volatile, JVM делает за нас две вещи:
Барьер StoreStore вставляется перед каждой операцией энергозависимой записи, а барьер StoreLoad вставляется после операции записи.
Барьеры LoadLoad вставляются перед каждой операцией чтения энергозависимой памяти, а барьеры LoadStore вставляются после операции чтения.
Все еще используя приведенный выше пример:
На этот раз используйте volatile для изменения переменнойb
int a = 0;//语句1
volatile int b = 1;//语句2
//在线程1中执行的语句
a = 10; //语句3
b = 20; //语句4
//在线程2中执行的语句
if(b == 20){
System.out.print("a此时已经是10了");
}
Оператор в потоке 1 после компиляции будет выглядеть примерно так
a = 10; //语句3
----------- StoreStore屏障 ---------------
b = 20; //语句4
----------- StoreLoad屏障 ---------------
В связи с наличием барьера,语句3и语句4не будет переупорядочиваться инструкциями, так что когда b=20, a будет присвоено значение 10. Тогда в этой программе нет проблем с безопасностью потоков.
3.1.3 Влияние барьеров памяти на производительность
Барьер памяти не позволяет ЦП использовать методы оптимизации для уменьшения задержки операций с памятью, и необходимо учитывать возникающее в результате снижение производительности. Для наилучшей производительности лучше всего разбить решаемую задачу на модули, чтобы процессор мог выполнять задачи по модулям, а затем разместить все необходимые барьеры памяти на границах решаемых задач. Использование этого метода позволяет процессору выполнять блок задач без ограничений.
Чтобы понять, как volatile гарантирует видимость, вам нужно понять концепцию кеша ЦП.
3.2.1 Кэш процессора
мы знаемСкорость работы процессорачемСкорость чтения и записи памятиГораздо быстрее, что создает ситуацию, когда память не успевает за процессором, а значит, и за кэш-памятью процессора. Это временный обмен данными между ЦП и памятью.Наши распространенные ЦП имеют трехуровневый кэш, часто называемый L1, L2 и L3.
На следующем рисунке представлена концептуальная модель кэш-памяти процессора Intel Core i7 (рисунок из «Углубленного понимания компьютерных систем»)
Когда система работает, ЦП выполняет следующие вычисления:
Программа и данные загружаются в оперативную память
Инструкции и данные загружаются в кэш ЦП
Процессор выполняет инструкцию и записывает результат в кеш.
Данные в кеше записываются обратно в основную память
В приведенной выше модели кэширования могут легко возникнуть проблемы, когда несколько ядер выполняют задачу одновременно. например.
Ядро 0 сначала считывает переменную a из памяти.
Core 3 также считывает переменную a из памяти.
Core 0 модифицирует переменную a и синхронизирует ее с основной памятью.
Core 3 начинает использовать переменную a, но значение остается прежним.
Для решения такого рода проблем существуют специфичные для ЦПпротокол МЭСИ.
3.2.2 Протокол МЭСИ
В ранних процессорах это реализовывалось добавлением LOCK# к шине (также известной какавтобусный замок). Когда ЦП работает с данными в своем кэше, он посылает на шину сигнал блокировки. В это время все процессоры не будут обрабатывать соответствующие данные в своих собственных кешах после получения этого сигнала.Когда операция будет завершена и блокировка снята, все процессоры перейдут в память для получения последнего обновления данных.
Но этот метод слишком дорог, поэтому Intel разработала протокол когерентности кэша, который называется протоколом MESI. его методСохраните бит флага в кэше ЦП, этот бит флага имеет четыре состояния:
M: Изменить, изменить кеш, текущий кеш процессора был изменен, то есть он не соответствует данным в памяти;
E: эксклюзивный, эксклюзивный кеш, текущий кеш ЦП согласуется с данными в памяти, а другие процессоры не имеют доступных данных кеша;
S: совместное использование, общий кеш, копия, согласованная с памятью, несколько наборов кешей могут иметь общие сегменты кеша для одного и того же адреса памяти одновременно;
I: Invalid, недопустимый кеш, что означает, что кеш в ЦП больше нельзя использовать.
Чтение ЦП следует следующим пунктам:
Если состояние кеша равно I, то читать из памяти, в противном случае читать непосредственно из кеша.
Если ЦП, чей кэш находится в M или E, имеет операции чтения из других ЦП, он записывает в память свой собственный кэш и устанавливает свое состояние в S.
ЦП может изменять данные в кеше только в том случае, если состояние кеша равно M или E. После изменения состояние кеша становится M.
Типичный пример:
Когда ЦП записывает данные, если он обнаруживает, что рабочая переменная является общей переменной, то есть копия переменной существует в других ЦП, он посылает сигнал другим ЦП, чтобы установить строку кэша переменной в недопустимое состояние. Когда другие ЦП используют эту переменную, они сначала обнюхивают, есть ли сигнал о том, что переменная была изменена, и когда обнаружится, что строка кэша этой переменной недействительна, он перечитает переменную из памяти.
3.2.3 Принцип видимости летучих
После понимания вышеизложенного легко понять, как реализована volatile.
Во-первых, когда общая переменная, измененная ключевым словом volatile, преобразуется в язык ассемблера, будет добавлена инструкция с префиксом блокировки.
Когда ЦП находит эту инструкцию, он немедленно делает две вещи:
Немедленная запись данных текущей строки кэша ядра в память
Данные, кэшированные в других ядрах, становятся недействительными через протокол MESI, поэтому другие потоки также должны повторно считывать данные из памяти.
Здесь также было введено много изменчивых, и, наконец, разница между ним и синхронизированным.
Чтобы узнать больше о синхронизации, нажмитездесь.
volatile — это модификатор переменной, а synchronized действует на фрагмент кода или метод.
Volatile просто синхронизирует значение переменной между памятью потока и «основной» памятью; synchronized синхронизирует значение всех переменных, блокируя и разблокируя монитор. Очевидно, что синхронизация потребляет больше ресурсов, чем volatile.
Volatile не гарантирует атомарности, но может гарантировать видимость и упорядоченность (реализованные барьерами памяти). Synchronized может гарантировать атомарность, видимость и упорядоченность.当你和面试官说到这里时,你最好清楚里面的具体细节,例如是从何种角度来看的有序性,以及如何实现的该特性,不然面试官很容易被问住的。
Эпилог
Пока на этом содержимое volatile закончилось.Если в тексте ошибка, или есть другиеvolatileЕсли более важный контент не представлен, пожалуйста, оставьте сообщение в области комментариев, чтобы обменяться мнениями и учиться вместе.