предисловие
Будь то в интервью или фактическом развитииvolatile
Это навык, которым следует овладеть.
Давайте сначала посмотрим, почему появляется это ключевое слово.
видимость памяти
потому чтоJava
модель памяти (JMM
) предусматривает, что все переменные хранятся в основной памяти, а каждый поток имеет свою рабочую память (кеш).
Когда поток работает, ему необходимо скопировать данные из основной памяти в рабочую память. Таким образом, любая операция с данными основана на рабочей памяти (повышение эффективности) и не может напрямую оперировать данными в основной памяти и рабочей памяти других потоков, а затем обновлять обновленные данные в основной памяти.
Упомянутую здесь основную память можно просто рассматривать каккуча памяти, а рабочую память можно рассматривать какстек памяти.
Как показано ниже:
Следовательно, во время параллельной работы данные, считанные потоком B, могут оказаться данными до обновления потока A.
Очевидно, что это обязательно пойдет не так, поэтомуvolatile
Появляется эффект:
когда переменная
volatile
При декорировании любая запись потока в него будет немедленно сброшена в основную память, а данные в потоке, кэшировавшем переменную, будут принудительно очищены, а самые последние данные должны быть повторно прочитаны из основной памяти.
volatile
После модификации вместо того, чтобы позволить потоку получать данные напрямую из основной памяти, все равно необходимо скопировать переменную в рабочую память.
Приложения видимости памяти
Когда нам нужно общаться между двумя потоками в соответствии с основной памятью, необходимо использовать коммуникационную переменнуюvolatile
модифицировать:
public class Volatile implements Runnable{
private static volatile boolean flag = true ;
@Override
public void run() {
while (flag){
System.out.println(Thread.currentThread().getName() + "正在运行。。。");
}
System.out.println(Thread.currentThread().getName() +"执行完毕");
}
public static void main(String[] args) throws InterruptedException {
Volatile aVolatile = new Volatile();
new Thread(aVolatile,"thread A").start();
System.out.println("main 线程正在运行") ;
TimeUnit.MILLISECONDS.sleep(100) ;
aVolatile.stopThread();
}
private void stopThread(){
flag = false ;
}
}
Основной поток изменяет бит флага, чтобы поток A немедленно останавливался, если он бесполезен.volatile
модификация, могут быть задержки.
Но здесь есть недоразумение: такой способ использования легко вызывает у людей ощущение, что:
правильно
volatile
Измененные переменные являются потокобезопасными для параллельных операций.
Здесь важно подчеркнуть,volatile
а такжене можетГарантированная безопасность потока!
Следующая процедура:
public class VolatileInc implements Runnable{
private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性
//private static AtomicInteger count = new AtomicInteger() ;
@Override
public void run() {
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet() ;
}
}
public static void main(String[] args) throws InterruptedException {
VolatileInc volatileInc = new VolatileInc() ;
Thread t1 = new Thread(volatileInc,"t1") ;
Thread t2 = new Thread(volatileInc,"t2") ;
t1.start();
//t1.join();
t2.start();
//t2.join();
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet();
}
System.out.println("最终Count="+count);
}
}
Когда наши три потока (t1, t2, main) одновременноint
При накоплении вы обнаружите, что конечное значение будет меньше 30000.
Это потому, что хотя
volatile
Видимость памяти гарантируется, и значение, полученное каждым потоком, является последним значением, ноcount ++
Эта операция не является атомарной, и операции, связанные с получением, увеличением и присвоением значений, не могут выполняться одновременно.
-
Так что подумайте, что достижение потокобезопасности может заставить эти три потока выполняться последовательно (на самом деле это один поток без преимуществ многопоточности).
-
также можно использовать
synchronize
Или блокировки для обеспечения атомарности. -
также можно использовать
Atomic
в сумкеAtomicInteger
заменитьint
, который используетCAS
алгоритм, обеспечивающий атомарность.
перестановка инструкций
Видимость памяти простоvolatile
одна из семантик , это также предотвращаетJVM
Выполните оптимизацию перестановки команд.
Возьмем псевдокод:
int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3
Особенно простой фрагмент кода, в идеале порядок выполнения будет таким:1>2>3
. Но возможно, что порядок выполнения после оптимизации JVM станет2>1>3
.
Можно обнаружить, что независимо от того, как оптимизирована JVM, предпосылка состоит в том, чтобы гарантировать, что конечный результат в одном потоке останется неизменным.
Может быть, здесь нет никакой проблемы, давайте посмотрим на следующий псевдокод:
private static Map<String,String> value ;
private static volatile boolean flag = fasle ;
//以下方法发生在线程 A 中 初始化 Map
public void initMap(){
//耗时操作
value = getMapValue() ;//1
flag = true ;//2
}
//发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
sleep() ;
}
//dosomething
doSomeThing(value);
}
Проблема видна здесь, когдаflag
не былvolatile
При изменении,JVM
переставляет 1 и 2, в результате чегоvalue
Он может использоваться потоком B до его инициализации.
так что добавьvolatile
Впоследствии такая реорганизация оптимизации может быть предотвращена, чтобы обеспечить правильность бизнеса.
Применение перестановки инструкций
Классический сценарий использования — двойная отложенная одноэлементная загрузка:
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//防止指令重排
singleton = new Singleton();
}
}
}
return singleton;
}
}
здесьvolatile
Ключевое слово в основном для предотвращения перестановки инструкций.
Если не,singleton = new Singleton();
, этот код фактически разделен на три шага:
- Выделить место в памяти. (1)
- Инициализировать объект. (2)
- будет
singleton
Объект указывает на выделенный адрес памяти. (3)
плюсvolatile
Это сделано для того, чтобы три вышеуказанных шага выполнялись последовательно.Наоборот, возможно, что второй шаг выполняется до третьего шага.Возможно, что одноэлементный объект, полученный потоком, не был инициализирован, так что сообщается об ошибке.
Суммировать
volatile
существуетJava
Он часто используется в параллелизме, напримерAtomic
в упаковкеvalue
,так же какAbstractQueuedLongSynchronizer
серединаstate
определяются какvolatile
для обеспечения видимости памяти.
Полное понимание этого может очень помочь, когда мы пишем параллельные программы.
Дополнительный
Недавно я обобщил некоторые знания, связанные с Java, и заинтересованные друзья могут поддерживать их вместе.