На собеседованиях по Java интервьюеры любят задавать вопросы, связанные с ключевым словом volatile. После многих интервью вы когда-нибудь задумывались, почему они так часто задают вопросы о ключевом слове volatile? И вы, как интервьюер, не могли бы вы также рассмотреть возможность использования ключевого слова volatile в качестве точки входа?
Почему любовь спрашивает о ключевом слове volatile
Интервьюеры, которые любят спрашивать о ключевом слове volatile, в большинстве случаев имеют определенную основу, потому что volatile используется в качестве точки входа. В направлении параллелизма вы, конечно, также можете перейти к параллельному программированию на Java.
Поэтому у тех, кто понимает вопрос, есть способ задавать вопросы. Итак, давайте взглянем на общий дизайн ключевого слова volatile: видимость памяти (функция JMM), атомарность (функция JMM), запрет на переупорядочивание инструкций, параллелизм потоков и разницу между synchronized... и затем копнем глубже. может включать байт-код, JVM и т. д.
Но к счастью, если вы уже изучили статьи серии JVM в паблике WeChat «Program New Horizon», вышеперечисленные пункты знаний уже не являются проблемой, и правильным должен быть обзор. Затем в форме вопроса интервьюера попробуйте ответить, не глядя на ответ, чтобы увидеть, как происходит эффект обучения. Серия смертельных вопросов, начните...
Интервьюер: Расскажите о характеристиках изменчивого ключевого слова.
Общие переменные, измененные volatile, имеют следующие две характеристики:
- Гарантирует видимость в памяти операций над этой переменной разными потоками;
- Отключить переупорядочивание инструкций;
Ответ очень хороший, и он указывает на две основные характеристики ключевого слова volatile. Продолжайте подробно об этих двух характеристиках.
Интервьюер: Что такое видимость памяти? Можете ли вы привести пример?
Этот вопрос касается модели памяти Java (JVM) и ее особенностей видимости памяти, которые будут обсуждаться в предыдущей серии "Подробная модель памяти Java (JMM)"и"Подробное объяснение принципов модели памяти Java.", чтобы разобраться с некоторым содержанием в ответе.
Сначала поговорим о модели памяти:Спецификация виртуальной машины Java пытается определить модель памяти Java (JMM), чтобы скрыть различия в доступе к памяти различного оборудования и операционных систем, чтобы программы Java могли достигать согласованных эффектов доступа к памяти на различных платформах..
Модель памяти Java заключается в том, чтобы синхронизировать новое значение обратно в основную память после изменения переменной, обновить значение переменной из основной памяти до того, как переменная будет прочитана, и использовать основную память в качестве среды передачи. Можно привести пример процесса видимости памяти.
В локальной памяти A и B есть копии общей переменной x в основной памяти, обе с начальным значением 0. После выполнения потока A значение x обновляется до 1 и сохраняется в локальной памяти A. Когда потоку A и потоку B необходимо взаимодействовать, поток A сначала обновляет значение x=1 из локальной памяти в основную память, и значение x в основной памяти становится равным 1. Затем поток B переходит в основную память для чтения обновленного значения x, и значение x локальной памяти потока B также становится равным 1.
Наконец, что не менее важно, видимость:Видимость означает, что когда поток изменяет значение общей переменной, другие потоки могут немедленно узнать об изменении..
Это верно как для обычных переменных, так и для изменчивых переменных, за исключением того, чтоИзменчивая переменная гарантирует, что новое значение может быть немедленно синхронизировано с основной памятью, а также сразу же обновляется из основной памяти при использовании, что обеспечивает видимость переменной во время многопоточной работы.. Обычные переменные не гарантируются.
Интервьюер: Говоря о JMM и наглядности, не могли бы вы рассказать о других особенностях JMM?
Мы знаем, что JMM обладает атомарностью и упорядоченностью в дополнение к видимости.
атомарностьТо есть операция или ряд непрерывны. Даже в случае нескольких потоков после запуска операции другие потоки не могут вмешиваться в нее.
Например, для статической переменной int x два потока присваивают ей значения одновременно, потоку A присваивается значение 1, а потоку B присваивается значение 2, независимо от того, как поток выполняется, окончательный значение x равно 1 или 2, поток A и поток Между операциями между B нет помех, которые являются атомарными операциями и не могут быть прерваны.
Порядок в модели памяти Java можно резюмировать в следующем предложении: если наблюдается в этом потоке, все операции упорядочены, а если один поток наблюдается в другом потоке, все операции не в порядке.
упорядоченностьЭто означает, что для однопоточного исполняемого кода выполнение выполняется последовательно. Однако в многопоточной среде может возникнуть явление нарушения порядка, поскольку в процессе компиляции происходит «перестановка инструкций», и перестроенные инструкции могут располагаться не в том же порядке, что и исходные инструкции.
Таким образом, первая половина приведенного выше предложения относится к гарантии последовательного семантического выполнения внутри потока, а вторая половина предложения относится к явлению «переупорядочения порядка» и «задержки синхронизации между рабочей памятью и основной памятью».
Интервьюер: Вы много раз упоминали о перестановке команд, можете привести пример?
Чтобы повысить эффективность выполнения программы, ЦП и компилятор позволяют оптимизировать инструкции в соответствии с определенными правилами. Однако между логиками кода существует определенная последовательность, и при параллельном выполнении будут получены разные результаты в соответствии с разной логикой выполнения.
Например, чтобы проиллюстрировать возможное явление перераспределения в многопоточности:
class ReOrderDemo {
int a = 0;
boolean flag = false;
public void write() {
a = 1; //1
flag = true; //2
}
public void read() {
if (flag) { //3
int i = a * a; //4
……
}
}
}
В приведенном выше коде, когда выполняется один поток, метод чтения может получить значение флага для оценки и получить ожидаемый результат. Но в случае многопоточности могут получиться другие результаты. Например, когда поток A выполняет операцию записи, из-за перестановки инструкций порядок выполнения кода в методе записи может стать следующим:
flag = true; //2
a = 1; //1
То есть сначала может быть назначен флаг, а затем назначен a. Это не влияет на окончательный вывод в одном потоке.
Но если в то же время поток B вызывает метод чтения, то возможно, что флаг истинен, но a по-прежнему равен 0. В это время результатом ввода операции четвертого шага является 0, а не ожидаемая 1 .
Переменная, измененная ключевым словом volatile, запрещает операцию перестановки инструкций, тем самым в определенной степени устраняя проблему многопоточности.
Интервьюер: Гарантирует ли volatile атомарность?
Volatile гарантирует видимость и упорядоченность (запрещает перестановку инструкций), поэтому можно ли гарантировать атомарность?
Volatile не гарантирует атомарность, он атомарен только для чтения/записи одной volatile переменной, но не для составных операций, таких как i++.
Следующий код, интуитивно говоря, чувствует, что выходной результат равен 10000, но на самом деле это не гарантируется, потому что операция inc++ является составной операцией.
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
Предположим, что поток A считывает значение inc равным 10, но заблокирован, так как переменная не изменена и правило volatile не срабатывает. Поток B в это время также считывает значение inc.Значение inc в основной памяти по-прежнему равно 10, и он автоматически увеличивает его, а затем сразу же записывает обратно в основную память со значением 11. В это время выполняется поток A. Поскольку в рабочей памяти хранится 10, он продолжает автоинкремент, а затем записывает обратно в основную память, и снова записывается 11. Таким образом, несмотря на то, что два потока выполняют функцию увеличения() дважды, результат добавляется только один раз.
Некоторые люди говорят, не делает ли volatile строку кэша недействительной? Но здесь поток A не изменяет значение inc после чтения, а поток B по-прежнему равен 10 при чтении. Кто-то еще сказал, что поток B записывает 11 обратно в основную память, не делает ли это недействительной строку кэша потока A? Только когда выполняется операция чтения и обнаруживается, что строка кэша недействительна, будет считано значение основной памяти, а операция чтения потока A была выполнена до записи потока B, поэтому здесь поток A может только продолжать делать свою собственную операцию.
В этом случае можно использовать только атомарные классы операций synchronized, Lock или atomic в параллельном пакете.
Интервьюер: Я только что упомянул синхронизированные, можете ли вы рассказать о разнице между ними?
- Суть volatile в том, чтобы сообщить JVM, что значение текущей переменной в регистре (рабочей памяти) неопределенно и его нужно считать из основной памяти; synchronized — в блокировке текущей переменной, только текущий поток может получить доступ к переменная, а другие потоки заблокированы.
- volatile можно использовать только на уровне переменной, synchronized можно использовать на уровне переменной, метода и класса;
- Volatile может обеспечить только видимость модификации переменных и не может гарантировать атомарность, в то время как синхронизация может гарантировать видимость модификации и атомарность переменных;
- Volatile не приведет к блокировке потока, а synchronized может привести к блокировке потока.
- Переменные, помеченные как volatile, не оптимизируются компилятором; переменные, помеченные как синхронизированные, могут быть оптимизированы компилятором.
Интервьюер: Можете ли вы привести другие примеры роли volatile?
Пример реализации одноэлементного паттерна, типичная блокировка с двойной проверкой (DCL):
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance==null) { // 1
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton(); // 2
}
}
return instance;
}
}
Это ленивый одноэлементный шаблон, объекты создаются только при использовании, а volatile добавляется к экземпляру, чтобы избежать переупорядочения инструкций для операций инициализации.
Зачем использовать синхронизированный, но также использовать изменчивый? В частности, хотя синхронизация обеспечивает атомарность, она не гарантирует правильности переупорядочивания инструкций.Поток А выполнит инициализацию, но это может быть потому, что в конструкторе слишком много операций, поэтому экземпляр экземпляра потока А еще не создан. Он выходит, но он назначен (то есть 2 операции в коде, сначала выделить место в памяти, а потом построить объект).
И в это время подошел поток B (операция кода 1, обнаружил, что экземпляр не нулевой), ошибочно подумал, что экземпляр был создан, только чтобы обнаружить, что экземпляр не был инициализирован. Имейте в виду, что хотя наши потоки могут гарантировать атомарность, программа может выполняться на многоядерном процессоре.
резюме
Конечно, у ключевого слова volatile есть и другие расширения: например, когда дело касается JMM, оно может быть расширено до различия между моделями памяти JMM и Java, когда дело доходит до атомарности, оно может быть расширено до того, как просматривать байт-код класса, а когда дело доходит до параллелизма, его можно расширить до Метод параллелизма потоков является всеобъемлющим.
На самом деле, не только на собеседовании, но и при получении знаний можно обратиться к этому мышлению на собеседовании и задать еще несколько «почему». Расширьте точку через «почему» в сеть знаний.
Оригинальная ссылка: "Ключевое слово volatile, которое чаще всего задают интервьюеры Java》
Серия статей "Интервьюер":
- "Подробное объяснение структуры памяти JVM》
- "Интервьюер, перестаньте спрашивать меня "Механизм сборки мусора Java GC"》
- "Интервьюер, Структура памяти Java8 JVM изменилась, постоянное преобразование в метапространство》
- "Интервьюер, перестаньте спрашивать меня "сборщик мусора Java"》
- "Загрузчик классов виртуальной машины Java и родительский механизм делегирования》
- "Подробная модель памяти Java (JMM)》
- "Подробное объяснение принципов модели памяти Java.》
- "Ключевое слово volatile, которое чаще всего задают интервьюеры Java》