вопрос
(1) Как volatile гарантирует видимость?
(2) Как volatile запрещает изменение порядка?
(3) Принцип реализации изменчивого?
(4) Дефекты летучих?
Введение
Можно сказать, что Volatile является самым легким механизмом синхронизации, предоставляемым виртуальной машиной Java, но его нелегко правильно понять, так что многие люди не привыкли его использовать, и они всегда используют синхронизированные или другие блокировки для решения многозадачных задач. проблемы с резьбой..
Понимание семантики volatile имеет большое значение для понимания характеристик многопоточности, поэтому брат Тонг написал статью, объясняющую семантику volatile.
Семантика 1: Видимость
Когда мы ранее представили модель памяти Java, мы сказали, что видимость означает, что когда поток изменяет значение общей переменной, другие потоки могут немедленно воспринять это изменение.
Объяснение модели памяти Java см. в [JMM (модель памяти Java) из мертвой серии синхронизации Java.
Обычные переменные не могут сразу это воспринять.Передачу значений переменных между потоками нужно совершать через основную память.Например,поток А модифицирует значение обычной переменной,а потом записывает обратно в основную память.Другой поток Б может прочитать значение новой переменной только после завершения обратной записи потока А и затем прочитать значение переменной из основной памяти, то есть новая переменная может быть видна потоку В.
В этот период могут возникнуть несоответствия, такие как:
(1) Поток A не выполняет обратную запись сразу после завершения модификации;
(Строка A изменяет значение переменной x на 5, но оно не было записано обратно, а поток B считывает старое значение из основной памяти в 0)
(2) поток B по-прежнему использует значение в своей собственной рабочей памяти вместо того, чтобы сразу считывать значение из основной памяти;
![volatile](https://s3.timeweb.com/newworld58-e1e8f297-7d39-4eff-9f5d-42281e40a914/UnderSkyWeb-27acc05ba0219d0240a80a4c0bb010e3968c4abf72b57988db987c040200e94caf17eab5.png)
(Поток A записывает обратно значение переменной x в основную память, но поток B не считывал значение основной памяти и по-прежнему использует старое значение 0 для работы)
Исходя из двух вышеперечисленных ситуаций, обычные переменные не могут сразу это воспринять.
Однако volatile переменные могут сделать это сразу, то есть volatile может гарантировать видимость.
Модель памяти Java предусматривает, что каждое изменение volatile-переменной должно быть немедленно записано обратно в основную память, и каждое использование volatile-переменной должно обновлять последнее значение из основной памяти.
![volatile](https://s3.timeweb.com/newworld58-e1e8f297-7d39-4eff-9f5d-42281e40a914/UnderSkyWeb-67ce076e5fe3f654d288a877baadf18a38f5fc31fe956333d9b0267b8b5c4476b79f299f.png)
Видимость volatile можно продемонстрировать на следующем примере:
public class VolatileTest {
// public static int finished = 0;
public static volatile int finished = 0;
private static void checkFinished() {
while (finished == 0) {
// do nothing
}
System.out.println("finished");
}
private static void finish() {
finished = 1;
}
public static void main(String[] args) throws InterruptedException {
// 起一个线程检测是否结束
new Thread(() -> checkFinished()).start();
Thread.sleep(100);
// 主线程将finished标志置为1
finish();
System.out.println("main finished");
}
}
В приведенном выше коде для готовых переменных программа может завершиться нормально, если используется volatile, и программа никогда не завершится, если volatile не используется.
Потому что, когда volatile не используется, поток, в котором находится checkFinished(), каждый раз считывает значение переменной в свою рабочую память, это значение всегда равно 0, поэтому он никогда не выпрыгнет из цикла while.
При использовании volatile-модификации поток, в котором находится checkFinished(), каждый раз загружает самое последнее значение из основной памяти, а когда finish будет изменен на 1 основным потоком, он сразу это воспримет, а затем выскочит из цикла while.
Семантика 2: изменение порядка запрещено
Когда мы ранее представили модель памяти Java, мы сказали, что упорядоченность в Java можно резюмировать одним предложением: если наблюдается в этом потоке, все операции упорядочены; если наблюдается в другом потоке, все операции не в порядке.
Первая половина предложения относится к семантике последовательного выполнения в потоке, а вторая половина предложения относится к явлению «переупорядочения инструкций» и «задержки синхронизации между рабочей памятью и основной памятью».
Объяснение модели памяти Java см. в [JMM (модель памяти Java) из мертвой серии синхронизации Java].
Обычные переменные лишь гарантируют, что правильные результаты могут быть получены во всех местах, которые зависят от результатов присваивания во время выполнения метода, но не могут гарантировать, что порядок операций присваивания переменных согласуется с порядком выполнения в программном коде, поскольку процесс выполнения метода потока. Неспособность понять это является «семантикой появления серийного номера в потоке».
Например, следующий код:
// 两个操作在一个线程
int i = 0;
int j = 1;
Приведенные выше два предложения не имеют зависимостей.Чтобы в полной мере использовать вычислительную мощность ЦП во время выполнения, JVM может выполняться первым.int j = 1;
Это предложение, то есть переупорядочивание, но оно не уловимо внутри треда.
Вроде бы и не влияет, а если дело в многопоточной среде?
Давайте посмотрим на другой пример:
public class VolatileTest3 {
private static Config config = null;
private static volatile boolean initialized = false;
public static void main(String[] args) {
// 线程1负责初始化配置信息
new Thread(() -> {
config = new Config();
config.name = "config";
initialized = true;
}).start();
// 线程2检测到配置初始化完成后使用配置信息
new Thread(() -> {
while (!initialized) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
}
// do sth with config
String name = config.name;
}).start();
}
}
class Config {
String name;
}
Этот пример очень прост: поток 1 отвечает за инициализацию конфигурации, а поток 2 определяет, что конфигурация инициализирована, и использует ее для каких-либо действий.
В этом примере, если initialized не изменен с помощью volatile, может произойти переупорядочение. Например, значение initialized установлено в true перед инициализацией конфигурации, так что поток 2 считывает это значение как true и использует конфигурацию. , может возникнуть ошибка. происходят в это время.
(Этот пример здесь используется только для иллюстрации переупорядочения, и его трудно отобразить в реальной среде выполнения.)
Благодаря этому примеру брат Тонг считает, что каждый лучше понимает, что «если наблюдать в этом потоке, все операции в порядке; если наблюдать в другом потоке, все операции не в порядке».
Таким образом, изменение порядка выполняется с точки зрения другого потока, поскольку в этом потоке влияние изменения порядка не может быть воспринято.
Переупорядочивать переменную volatile запрещено, это может гарантировать, что фактическая работа программы выполняется в порядке кода.
Реализация: барьеры памяти
Выше упоминалось, что volatile может гарантировать видимость и запрещать изменение порядка, так как же это реализовано?
Ответ: барьеры памяти.
Барьеры памяти служат двум целям:
(1) Предотвратить изменение порядка инструкций по обе стороны барьера;
(2) Принудительная запись данных из буфера записи/кэша обратно в основную память для аннулирования соответствующих данных в кеше;
Что касается точки знания «барьера памяти», то взгляды различных великих богов не совсем совпадают, поэтому брат Тонг не будет говорить об этом здесь.Если вам интересно, вы можете прочитать следующие статьи:
(Обратите внимание, что официальная учетная запись не разрешает исходящие ссылки, поэтому вы можете только скопировать ссылку в браузер, чтобы прочитать ее, и вам также может потребоваться научный серфинг в Интернете)
(1) Поваренная книга JSR-133 для разработчиков компиляторов Дуга Ли
A.Oswego.quota/login/JMM/cook…
Дуг Леа — автор параллельных пакетов Java, Дэниел!
(2) «Барьеры/заборы памяти» Мартина Томпсона
Mechanical-sympathy.blogspot.com/2011/07/ Что…
Мартин Томпсон фокусируется на повышении производительности до предела, уделяя внимание проблемам аппаратного уровня, например, как избежать ложного обмена, Дэниел!
Его адрес в блоге указан выше, и в нем много базовых знаний.Если вам интересно, вы можете пойти и посмотреть его.
(3) «Барьеры памяти и параллелизм JVM» Денниса Бирна
Woohoo.info Q.com/articles/ Что…
Это статья на англоязычном сайте InfoQ, я считаю, что она хорошо написана, она в основном объединяет две вышеприведенные точки зрения и анализирует реализацию барьеров памяти с уровня ассемблера.
В настоящее время объяснение барьеров памяти на внутреннем рынке в основном не превышает этих трех статей, включая введение в соответствующие книги.
Давайте все же рассмотрим пример, чтобы понять влияние барьеров памяти:
public class VolatileTest4 {
// a不使用volatile修饰
public static long a = 0;
// 消除缓存行的影响
public static long p1, p2, p3, p4, p5, p6, p7;
// b使用volatile修饰
public static volatile long b = 0;
// 消除缓存行的影响
public static long q1, q2, q3, q4, q5, q6, q7;
// c不使用volatile修饰
public static long c = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (a == 0) {
long x = b;
}
System.out.println("a=" + a);
}).start();
new Thread(()->{
while (c == 0) {
long x = b;
}
System.out.println("c=" + c);
}).start();
Thread.sleep(100);
a = 1;
b = 1;
c = 1;
}
}
В этом коде a и c не изменяются с помощью volatile, а b изменяется с помощью volatile, и мы добавляем 7 длинных полей между a/b и b/c, чтобы устранить эффект ложного совместного использования.
Для получения соответствующих знаний о псевдосовместном использовании вы можете проверить статьи, написанные Тонг Ге до [Разное Что такое ложный обмен?].
В цикле while двух потоков a и c мы получаем b, знаете что? если поставитьlong x = b;
Удалить эту строку? Попробуйте.
Брат Тонг прямо сказал здесь вывод: сфера влияния volatile-переменной не только включает себя, но и влияет на чтение и запись значений переменных над и под ней.
дефект
Выше мы представили две семантики ключевого слова volatile Итак, является ли ключевое слово volatile универсальным?
Конечно, нет, разве вы не забыли о трех характеристиках непротиворечивости, которые мы упоминали в главе о модели памяти?
Непротиворечивость в основном включает три характеристики: атомарность, видимость и упорядоченность.
Ключевое слово volatile может гарантировать видимость и упорядоченность, так может ли volatile гарантировать атомарность?
См. пример ниже:
public class VolatileTest5 {
public static volatile int counter = 0;
public static void increment() {
counter++;
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100);
IntStream.range(0, 100).forEach(i->
new Thread(()-> {
IntStream.range(0, 1000).forEach(j->increment());
countDownLatch.countDown();
}).start());
countDownLatch.await();
System.out.println(counter);
}
}
В этом коде мы запускаем 100 потоков для увеличения счетчика в 1000 раз, что в сумме должно составить 100 000, но фактический результат выполнения никогда не достигнет 100 000.
Давайте посмотрим на байт-код метода increment() (можно просмотреть плагины, связанные с загрузкой IDEA):
0 getstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter>
3 iconst_1
4 iadd
5 putstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter>
8 return
Вы можете видеть, что counter++ разбит на четыре инструкции:
(1) getstatic, получить текущее значение счетчика и поместить его в стек
(2) icont_1, значение типа int в стеке равно 1
(3) iaadd, добавьте два значения наверху стека
(4) putstatic, записать результат сложения обратно в счетчик
Поскольку счетчик изменяется с помощью volatile, getstatic обновит последнее значение из основной памяти, а putstatic также немедленно синхронизирует измененное значение с основной памятью.
Однако во время выполнения двух промежуточных шагов icont_1 и iadd значение counter могло быть изменено.В это время последнее значение в основной памяти не было перечитано, поэтому volatile не может гарантировать его атомарность в сценарий counter++. .
Ключевое слово volatile может гарантировать только видимость и упорядоченность, но не атомарность.Чтобы решить проблему атомарности, ее можно решить только путем блокировки или использования атомарных классов.
Кроме того, мы получаем сценарий, в котором используется ключевое слово volatile:
(1) Результат операции не зависит от текущего значения переменной или может гарантировать, что только один поток изменяет значение переменной;
(2) Переменные не должны участвовать в инвариантных ограничениях с другими переменными состояния.
Проще говоря, volatile само по себе не гарантирует атомарности, поэтому необходимо добавить другие ограничения, чтобы сделать саму сцену атомарной.
Например:
private volatile int a = 0;
// 线程A
a = 1;
// 线程B
if (a == 1) {
// do sth
}
a = 1;
Эта операция присваивания сама по себе является атомарной, поэтому ее можно модифицировать с помощью volatile.
Суммировать
(1) Ключевое слово volatile может гарантировать видимость;
(2) Ключевое слово volatile может обеспечить упорядоченность;
(3) ключевое слово volatile не может гарантировать атомарность;
(4) Нижний уровень ключевого слова volatile в основном реализуется через барьеры памяти;
(5) Сценарий использования ключевого слова volatile должен состоять в том, что сам сценарий является атомарным;
пасхальные яйца
Что касается трех статей о «барьере памяти», учитывая, что некоторые студенты не могут получить доступ к Интернету с научной точки зрения, брат Тонг специально скачал эти три статьи и разобрал их.
Обратите внимание на мой публичный аккаунт «Tong Ge Read Source Code», ответьте «volatile» в фоновом режиме и загрузите эти три материала.
Добро пожаловать, чтобы обратить внимание на мою общедоступную учетную запись «Брат Тонг читает исходный код», проверить больше статей из серии исходного кода и поплавать в океане исходного кода с братом Тонгом.
![qrcode](https://s3.timeweb.com/newworld58-e1e8f297-7d39-4eff-9f5d-42281e40a914/UnderSkyWeb-eb033bd4b9089752d34eb5fab88fd138c85d002a727182a40bb4d73b90e00c0c83cf3edf.jpg)