Synchronized — это тяжеловесная блокировка, а volatile — облегченная синхронизированная, обеспечивающая «видимость» общих переменных при многопоточной разработке. Если переменная использует volatile , это дешевле, чем использование synchronized, потому что это не вызывает переключения контекста потока и планирования.
Язык программирования Java позволяет потокам получать доступ к общим переменным.Чтобы гарантировать, что общая переменная может быть обновлена точно и последовательно, поток должен обеспечить, чтобы переменная была получена индивидуально через монопольную блокировку. С точки зрения непрофессионала, если переменная изменена с помощью volatile, Java может гарантировать, что все потоки увидят одно и то же значение переменной. Если поток обновляет переменную общего доступа, измененную volatile, другие потоки могут немедленно увидеть это обновление, что называется видимостью потока.
Понятия, связанные с моделью памяти
Понимание volatile на самом деле немного сложно, поскольку оно связано с моделью памяти Java, поэтому нам нужно понять концепцию модели памяти Java, прежде чем разбираться с volatile.
семантика операционной системы
Когда компьютер запускает программу, каждая инструкция выполняется в ЦП, а чтение и запись данных обязательно участвуют в процессе выполнения. Мы знаем, что данные, которые запускает программа, хранятся в основной памяти. В это время будет проблема. Чтение и запись данных в основную память не так быстры, как выполнение инструкций в ЦП. Если какое-либо взаимодействие необходимо Разберитесь с основной памятью, это сильно повлияет на эффективность, поэтому есть кеш процессора. Кэши ЦП уникальны для определенного ЦП и относятся только к потокам, работающим на этом ЦП.
С кешем ЦП, хотя проблема эффективности решена, он принесет новую проблему: непротиворечивость данных.
Когда программа запущена, копия данных, необходимых для операции, будет скопирована в кеш ЦП.При выполнении операции ЦП больше не будет иметь дело с основной памятью, а будет напрямую читать и записывать данные из кеша. Данные сбрасываются в основную память.
Возьмем простой пример:
i = i + 1;
Когда поток запускает этот код, он сначала считывает значение i из основной памяти (при условии, что i = 1 в это время), затем копирует копию в кэш ЦП, а затем ЦП делает + 1 (когда i = 2), затем записать данные i = 2 в кеш сообщений и, наконец, сбросить в основную память.
На самом деле нет никакой проблемы сделать это в одном потоке, но проблема в многопоточности. следующее:
假如有两个线程 A、B 都执行这个操作( i++ ),
Согласно нашему обычному логическому мышлению, значение i в оперативной памяти должно быть равно 3.
Но так ли это? проанализируйте, как показано ниже:
两个线程从主存中读取 i 的值( 假设此时 i = 1 ),到各自的高速缓存中,
然后线程 A 执行 +1 操作并将结果写入高速缓存中,最后写入主存中,此时主存 i = 2 。
线程B做同样的操作,主存中的 i 仍然 =2 。所以最终结果为 2 并不是 3 。
这种现象就是缓存一致性问题。
Существует два решения кэша последовательности:
通过在总线加 LOCK# 锁的方式
通过缓存一致性协议
С первым решением есть проблема, оно реализовано эксклюзивным образом, то есть, если шина заблокирована с помощью LOCK#, может работать только один ЦП, а остальные ЦП должны быть заблокированы, что неэффективно.
Вторая схема, Cache Coherency Protocol (MESI Protocol), гарантирует согласованность копий общих переменных, используемых в каждом кэше. Основная идея заключается в следующем: когда ЦП записывает данные, если он обнаруживает, что обрабатываемая переменная является общей переменной, он уведомляет другие ЦП о том, что строка кэша переменной недействительна, поэтому, когда другие ЦП читают переменную, они обнаружить, что строка кеша переменной недействительна.Недействительность перезагружает данные из основной памяти.
Модель памяти Java
Вышеизложенное объясняет, как обеспечить согласованность данных на уровне операционной системы.Давайте взглянем на модель памяти Java и немного изучим, какие гарантии она нам предоставляет, а также какие методы и механизмы предусмотрены в Java, чтобы позволить нам делать подробнее Поточное программирование может гарантировать правильность выполнения программы.
В параллельном программировании мы обычно сталкиваемся с тремя основными понятиями: атомарность, видимость и упорядоченность. Давайте посмотрим на volatile.
атомарность
Атомарность: То есть одна операция или несколько операций либо выполняются и процесс выполнения не будет прерван никакими факторами, либо нет.
Атомарность похожа на транзакцию в базе данных.Давайте рассмотрим простой пример ниже:
i = 0; // <1>
j = i ; // <2>
i++; // <3>
i = j + 1; // <4>
Какие из четырех вышеперечисленных операций являются атомарными, а какие нет? Если вы не очень хорошо в этом разбираетесь, то можете подумать, что все операции атомарны, на самом деле атомарной является только 1 операция, а остальные нет.
- В Java операции с переменными и присваивания для примитивных типов данных являются атомарными.
- Содержит две операции: читайте I и назначаем значение i к j.
- Содержит три операции: Read I Value, i + 1, назначить результаты +1 для i.
- то же, что
Итак, в 64-битной среде JDK чтение и запись 64-битных данных являются атомарными?
实现对普通long与double的读写不要求是原子的(但如果实现为原子操作也OK)
实现对volatile long与volatile double的读写必须是原子的(没有选择余地)
Кроме того, volatile не может гарантировать атомарность составных операций.
видимость
Видимость означает, что когда несколько потоков обращаются к одной и той же переменной, один поток изменяет значение переменной, а другие потоки могут сразу увидеть измененное значение.
Как было проанализировано выше, в многопоточной среде операции одного потока над общими переменными невидимы для других потоков.
Java предоставляет volatile, чтобы гарантировать видимость.
Когда переменная объявляется volatile, это указывает на недопустимую локальную память потока.
Когда поток изменяет общую переменную, она будет немедленно обновлена в основную память;
Когда другие потоки читают общую переменную, она считывается непосредственно из основной памяти.
И синхронизация, и блокировки гарантируют видимость.
упорядоченность
Упорядоченный: порядок, в котором выполняется программа, выполняется в порядке кода.
В модели памяти Java для эффективности состоит в том, чтобы позволить компилятору и процессору изменить порядок инструкций, это, конечно, переупорядочение не влияет на один резьбовый результат операции, но мульти-нить будет иметь удар.
Java предоставляет volatile, чтобы гарантировать определенный порядок. Самый известный пример — DCL (Double Checked Lock) в шаблоне singleton.
Анализ принципа изменчивости
Volatile может гарантировать видимость потока и обеспечивать определенный порядок, но не может гарантировать атомарность. В нижней части JVM volatile реализовано с использованием «барьеров памяти».
Приведенный выше абзац имеет два слоя семантики:
保证可见性、不保证原子性
禁止指令重排序
Первый уровень семантики представлен не будет, а следующий будет посвящен переупорядочению инструкций.
инструкция переупорядочение
Чтобы повысить производительность при выполнении программы, компилятор и процессор обычно выполняют инструкции по переупорядочению:
编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
Переупорядочивание инструкций не влияет на один поток, не влияет на результат работы программы, но влияет на корректность многопоточности. Поскольку переупорядочивание инструкций влияет на корректность многопоточного выполнения, нам нужно отключить переупорядочивание. Так как же JVM запрещает изменение порядка?
Это приводит к принципу «случись до».
- Правила порядка программы: внутри потока, в соответствии с порядком кода, операции, написанные впереди, происходят раньше операций, написанных сзади.
- Правила блокировки: операция разблокировки происходит до того, как следует операция блокировки той же блокировки.
- правила для изменчивой переменной: запись в изменчивую переменную происходит допозжеОперация чтения этой переменной. Обратите внимание на последнее.
- Правило доставки: если операция A происходит раньше операции B, а операция B происходит раньше операции C, то из этого следует, что операция A происходит раньше операции C.
- Правила запуска потока: метод запуска объекта Thread, происходит перед каждым действием этого потока.
- Правила прерывания потока: вызов метода прерывания потока происходит до того, как код прерванного потока обнаружит возникновение события прерывания.
- Правила завершения потока: все операции в потоке происходят до обнаружения завершения потока.Мы можем определить, что поток завершил выполнение через конец метода Thread.join() и возвращаемое значение Thread.isAlive().
- Правила финализации объекта: инициализация объекта завершена, происходит до запуска его метода finalize()
Давайте сосредоточимся на третьем пункте правила Volatile: операции записи в volatile-переменные происходят до последующих операций чтения.
Чтобы достичь семантики энергозависимой памяти, JMM изменит порядок, правила следующие:
当第二个操作是 volatile 写操作时,不管第一个操作是什么,都不能重排序。
这个规则,确保 volatile 写操作之前的操作,都不会被编译器重排序到 volatile 写操作之后。
Немного поняв принцип «случиться до», давайте ответим на вопрос, как JVM запрещает переупорядочивание?
观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,
加入volatile 关键字时,会多出一个 lock 前缀指令。
lock 前缀指令,其实就相当于一个内存屏障。
内存屏障是一组处理指令,用来实现对内存操作的顺序限制。
volatile 的底层就是通过内存屏障来实现的。
На следующем рисунке показаны барьеры памяти, необходимые для выполнения вышеуказанных правил:
Суммировать
Volatile выглядит просто, но разобраться в нем сложно, вот только базовое понимание.
Volatile немного легче, чем синхронизированный. В некоторых случаях он может заменить синхронизированный, но не может полностью заменить синхронизированный. Volatile можно использовать только в определенных ситуациях, и он должен соответствовать следующим двум условиям:
对变量的写操作,不依赖当前值。
该变量没有包含在具有其他变量的不变式中。
Volatile часто используется в следующих сценариях: переменные тега состояния, двойная проверка, один поток записывает несколько потоков для чтения.