В ленивом одноэлементном шаблоне только с блокировками с двойной проверкой и без volatile из-за指令重排序
проблема, я действительно не получу两个不同的单例
, но я получу“半个”单例
.
А volatile, играющий волшебную роль, вполне заслуженно может называться параллельным программированием на Java.«Наиболее часто встречающиеся ключевые слова», часто используемый для поддержания видимости памяти и предотвращения переупорядочивания инструкций.
поддерживать видимость памяти
Видимость памяти: все потоки могут видеть последнее состояние общей памяти.
Данные об отказах
Ниже приведен простой изменяемый целочисленный класс:
public class MutableInteger {
private int value;
public int get(){
return value;
}
public void set(int value){
this.value = value;
}
}
MutableInteger
не является потокобезопасным, потому чтоget
иset
Способ осуществляется при отсутствии синхронизации. Если поток 1 вызывает метод set, то вызывающий поток 2 может увидеть значение обновленного значения, а такжеможет не видеть.
Решение простое,value
объявлен какvolatile
Переменная:
private volatile int value;
Волшебное изменчивое ключевое слово
Ключевое слово magic volatile решает проблему магических устаревших данных.
Чтение и запись переменных Java
Java выполняется несколькими атомарными операциями工作内存
и主内存
взаимодействие:
- lock: действует в основной памяти, помечая переменную как эксклюзивную для потока.
- разблокировка: воздействует на основную память, освобождая монопольное состояние.
- чтение: воздействует на основную память, передавая значение переменной из основной памяти в рабочую память потока.
- load: воздействует на рабочую память и помещает значение переменной, переданное операцией чтения, в копию переменной рабочей памяти.
- использование: воздействовать на рабочую память и передавать значение переменной в рабочей памяти механизму выполнения.
- assign: воздействует на рабочую память, присваивая значение, полученное от исполнительного механизма, переменной в рабочей памяти.
- store: переменная, которая воздействует на рабочую память и передает значение переменной из рабочей памяти в основную память.
- запись: переменная, действующая в основной памяти, помещая значение переменной из операции сохранения в переменную в основной памяти.
Как volatile поддерживает видимость памяти
Особые правила для volatile:
- читать, загружать, использовать действия должныпоявляются по очереди.
- назначать, хранить, записывать действия должныпоявляются по очереди.
Итак, использование volatile-переменных гарантирует:
- Каждый
读取前
Последнее значение должно быть сброшено из основной памяти в первую очередь. - Каждый
写入后
Он должен быть немедленно синхронизирован обратно в основную память.
Это,Переменные, измененные ключевым словом volatile, всегда видят свое последнее значение.. Последняя модификация переменной v в потоке 1 видна потоку 2.
Предотвратить перестановку инструкций
на основе偏序关系
изHappens-Before内存模型
, технология перестановки инструкций значительно повышает эффективность выполнения программы, но также создает некоторые проблемы.
Проблема перестановки инструкций - частично инициализированные объекты
Шаблон отложенной загрузки синглтона и условия гонки
Один懒加载
из单例模式
Реализация выглядит следующим образом:
class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if ( instance == null ) { //这里存在竞态条件
instance = new Singleton();
}
return instance;
}
}
竞态条件
приведет кinstance
Ссылка назначается несколько раз, оставляя пользователю два разных синглтона.
DCL и частично инициализированные объекты
Для решения этой проблемы можно использоватьsynchronized
ключевые слова будутgetInstance
метод меняется на синхронный метод, ноТакой сериализованный синглтон невыносим. Так мои предшественники-обезьяны разработали его.DCL
(Double Check Lock), чтобы большинство запросов не попадали в блокирующий кодовый блок:
class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if ( instance == null ) { //当instance不为null时,仍可能指向一个“被部分初始化的对象”
synchronized (Singleton.class) {
if ( instance == null ) {
instance = new Singleton();
}
}
}
return instance;
}
}
Он «выглядит» идеально: уменьшает блокировку и позволяет избежать условий гонки. Неплохо, но на самом деле есть еще проблема -Когда экземпляр не нулевой, он все равно может указывать на"被部分初始化的对象"
.
Проблема заключается в этом простом операторе присваивания:
instance = new Singleton();
Это не атомарная операция. На самом деле его можно «абстрагировать» в следующие инструкции JVM:
memory = allocate(); //1:分配对象的内存空间
initInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
вышеОперация 2 зависит от операции 1, а операция 3 не зависит от операции 2., поэтому JVM может «оптимизировать» их重排序
, после переупорядочения следующим образом:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory); //2:初始化对象
Видно, что после перестановки инструкций операция 3 располагается перед операцией 2, т.е.Когда эталонный экземпляр указывает на память памяти, эта новая память не была инициализирована.- То есть ссылочный экземпляр указывает на "частично инициализированный объект". В этот момент, если другой поток вызывает метод getInstance,Поскольку экземпляр уже указал на область памяти, условие if оценивается как ложное, и метод возвращает ссылку на экземпляр., пользователь получает "половину" синглтона, инициализация которого еще не завершена.
Чтобы решить эту проблему, просто объявите instance как volatile переменную:
private static volatile Singleton instance;
Это,В ленивом одноэлементном шаблоне только с DCL и без volatile все еще есть ловушки параллелизма.. Я действительно не пойму.
两个不同的单例
, но я получу“半个”单例
(Инициализация не завершена).
Однако во многих книгах с интервью одноэлементный паттерн с ленивой загрузкой в лучшем случае углубляется в DCL, но вообще не упоминает volatile. Этот «на первый взгляд умный» механизм когда-то был высоко оценен подавляющим большинством моих соотечественников-обезьян, которые были новичками в мире Java.Когда я узнал, у кого я учился на своем старшем собеседовании на стажировке, я также с гордостью рассказал о Двойнике от голодных и голодный. Проверьте, выглядит действительно глупо сейчас. Для интервьюеров, изучающих параллелизм, реализация одноэлементного шаблона является хорошей отправной точкой.. Кажется, что он исследует шаблоны проектирования, но на самом деле ожидается, что вы будете отвечать от шаблонов проектирования до моделей параллелизма и памяти.
Как volatile предотвращает перестановку инструкций
ключевое слово volatile передано“内存屏障”
Чтобы инструкции не были отклонены.
Чтобы достичь семантики энергозависимой памяти, компилятор вставляет барьеры памяти в последовательность инструкций, чтобы запретить определенные типы переупорядочения процессора при создании байт-кода. Однако для компилятора установлено, что общее количество оптимальных компоновок для минимизации барьера вставки практически невозможно.С этой целью модель памяти Java использует консервативные стратегии.
Ниже приведена стратегия вставки барьера памяти JMM, основанная на консервативной стратегии:
- Вставьте барьер StoreStore перед каждой операцией энергозависимой записи.
- Вставляйте барьер StoreLoad после каждой операции энергозависимой записи.
- Вставьте барьер LoadLoad после каждого чтения volatile.
- Вставляйте барьер LoadStore после каждого чтения энергозависимых данных.
Передовой
Отвечая однажды на вышеуказанный вопрос, я забыл объяснить вопрос, который легко может вызвать путаницу:
Если есть такая проблема переупорядочивания, не возможна ли такая же проблема внутри синхронизированного блока?
то есть этот случай:
class Singleton {
...
if ( instance == null ) { //可能发生不期望的指令重排
synchronized (Singleton.class) {
if ( instance == null ) {
instance = new Singleton();
System.out.println(instance.toString()); //程序顺序规则发挥效力的地方
}
}
}
...
}
Звонитеinstance.toString()
метод, экземпляр также может не завершить инициализацию?
Прежде всего, пожалуйста, расслабьтесь,Несмотря на то, что блок синхронизированного кода будет переупорядочен, это не вызовет проблем с безопасностью потоков в рамках блока кода..
Модель памяти Happens-Before и правила порядка выполнения программ
Правило порядка выполнения программы: если операция A в программе предшествует операции B, то операция A в потоке будет выполнена до операции B.
Я сказал раньше,Такие проблемы с переупорядочением инструкций возникают только в модели памяти Happens-Before.. Модель памяти Happens-Before поддерживает несколько правил Happens-Before,程序顺序规则
Самые основные правила. Целевым объектом правила последовательности программ являются две операции А и В в программном коде, которыеГарантируется, что перестановка инструкций здесь не разрушит порядок операций A и B в коде, но не имеет ничего общего с порядком в разных кодах или даже разных потоках.
Итак, внутри синхронизированного блокаinstance = new Singleton()
Инструкции по-прежнему будут переупорядочены, но все инструкции после переупорядочения по-прежнему гарантированно будут вinstance.toString()
выполнял раньше. Далее, в одном потоке,if ( instance == null )
Он гарантированно выполняется перед блоком синхронизированного кода, но в многопоточности код в потоке 1if ( instance == null )
Однако взаимосвязь частичного порядка с блоком синхронизированного кода в потоке 2 отсутствует, поэтому переупорядочение инструкций внутри блока синхронизированного кода в потоке 2 не ожидается для потока 1, что приводит к ловушке параллелизма.
Аналогичные правила
volatile变量规则
,监视器锁规则
Ждать. Программисты могут借助
(Комплектация) Существующие правила Happens-Before для поддержания видимости памяти и предотвращения переупорядочения инструкций.
будь осторожен
Функция и принцип ключевого слова volatile кратко описаны выше, но проблема, которая легко возникает при использовании volatile:
Принимает изменчивые переменные за атомарные переменные.
Основная причина этого недоразумения заключается в том, чтоКлючевое слово volatile делает чтение и запись переменных «атомарными».. Однако этот атомныйОграничено чтением и записью переменных (включая ссылки), не может охватывать какие-либо операции с переменными.,который:
- Автоинкремент примитивных типов (таких как
count++
) и другие операции не являются атомарными. - любой вызов неатомарного члена объекта (включая
成员变量
и成员方法
) не является атомарным.
Если вы хотите, чтобы вышеуказанные операции были атомарными, вы можете только принять дополнительные меры, такие как блокировки и атомарные переменные.
Суммировать
Подводя итог, можно сказать, что принцип volatile, поддерживающий видимость памяти и предотвращающий переупорядочивание инструкций, по сути является одной и той же проблемой, и она также решается с помощью барьеров памяти. Дополнительные сведения см. в книгах по JVM.
Ссылаться на:
Ссылка на эту статью:Роль и принцип ключевого слова volatile
автор:обезьяна 007
Источник:monkeysayhi.github.io
Эта статья основана наCreative Commons Attribution-ShareAlike 4.0Выпущено по международному лицензионному соглашению, приветствуется перепечатка, вывод или использование в коммерческих целях, но авторство и ссылка на эту статью должны быть сохранены.