Основным содержанием этой статьи является общеизвестное значение в интервью Java: изменчивые ключевые слова. В этой статье подробно представлены все аспекты нестабильных ключевых слов. Я надеюсь, что после прочтения этой статьи вы сможете прекрасно решить связанные проблемы изменчивых ключевых слов.
В собеседованиях, связанных с Java, многим интервьюерам нравится изучать понимание интервьюера о параллелизме Java. Используя нестабильное ключевое слово в качестве небольшой точки входа, вы часто можете спросить модель памяти Java (JMM) и некоторые функции параллельного программирования Java. В глубине вы также можете изучить базовую реализацию JVM и знания, связанные с операционной системой. Давайте возьмем гипотетический процесс интервью, чтобы получить глубокое понимание ключевого слова Volitile!
Насколько я понимаю, общие переменные, модифицированные летучими, имеют следующие две характеристики:
1. Обеспечить видимость памяти различных потоков в операции переменной;
2. запретить на повторное распоряжение команды
Это о чем поговорить, поэтому я начну с модели памяти Java. Спецификация виртуальной машины Java пытается определить модель памяти Java (JMM), чтобы заблокировать различия доступа к памяти между различными аппаратными и операционными системами, чтобы программы Java могли достичь последовательных эффектов доступа к памяти на различных платформах. Проще говоря, поскольку процессор очень быстро выполняет инструкции, скорость доступа к памяти намного медленнее, и разница не является порядком, крупные парни, которые работают над процессором, добавили несколько слоев кэша в ЦП. В модели памяти Java приведенная выше оптимизация снова абстрагирована. JMM предусматривает, что все переменные находятся в основной памяти, аналогичной обычной памяти, упомянутой выше, и каждый поток содержит свою собственную рабочую память. Его можно рассматривать как регистр или кэш на процессоре для удобного понимания. Следовательно, операции потока в основном основаны на рабочей памяти. Они могут получить доступ только к своей собственной рабочей памяти, и они должны синхронизировать значение обратно к основной памяти до и после работы. Я даже не знаю, что я так сказал, возьмите лист бумаги, чтобы нарисовать:
При выполнении потока значение переменной будет сначала считываться из основной памяти, а затем загружено в копию в рабочей памяти, а затем передат ее в процессор для выполнения. После завершения выполнения копии в рабочей памяти будет назначено значение, и тогда значение в рабочей памяти будет передано обратно в основную память, а значение в основной памяти будет обновлено. Хотя использование рабочей памяти и основной памяти происходит быстрее, оно также вызывает некоторые проблемы. Например, посмотрите на следующий пример:
i = i + 1;
Предполагая, что начальное значение I составляет 0, когда его выполняет только один поток, результат определенно получит 1. Когда два потока выполнятся, получит ли результат 2? Это не обязательно так. Это может иметь место:
Поток 1: Загрузите I из основной памяти // i = 0 i + 1 // i = 1 Поток 2: Загрузите I из основной памяти // Потому что поток 1 не записал значение I обратно в основную память, я все еще 0 i + 1 // i = 1 Поток 1: Сохраните I в основную ветку памяти 2: Сохранить I в основную память
Если два потока следуют приведенному выше процессу выполнения, то последнее значение I на самом деле 1. Если последняя запись обратно является медленной, и вы можете прочитать значение I снова, это может быть 0, что является проблемой несоответствия кэша. Следующее - упомянуть вопрос, который вы только что задали. JMM в основном основан на том, как справиться с тремя характеристиками атомальности, видимости и порядка в процессе параллелистики. Решая эти три задачи, проблема несоответствия кэша может быть решена. И нестабильный связан с видимостью и упорядочением.
1. Атомность: в Java операциями чтения и назначения основных типов данных являются атомными операциями. Так называемые атомные операции означают, что эти операции непрерывны и должны быть завершены в течение определенного периода времени, или они не будут выполнены. например:
i = 2; j = i; i ++; i = i+1;
Среди четырех вышеперечисленных операций I = 2 - операция чтения, которая должна быть атомной операцией. J = я думаю, что это атомная операция. На самом деле, он разделен на два шага. Один из них - прочитать значение I, а затем назначить значение j. Это 2-ступенчатая операция. Это нельзя назвать атомной операцией. I ++ и I = I+ 1 на самом деле эквивалентны. Прочитайте значение I, добавьте 1 и напишите его обратно в основную память. Это 3-ступенчатая операция. Следовательно, в приведенном выше примере последнее значение может иметь много ситуаций, потому что оно не может удовлетворить атомность. Таким образом, есть только простое чтение. Назначение - это атомная операция или только числовое назначение. Если вы используете переменные, существует дополнительная операция, чтобы считать значение переменной. Исключением является то, что спецификация виртуальной машины позволяет обрабатывать 64-разрядные типы данных (длинный и двойной) в 2 операциях, но последняя реализация JDK все еще реализует атомные операции. JMM реализует только основную атомность. Операции, подобные I ++ выше, должны быть синхронизированы и блокируются, чтобы обеспечить атоманость всего кода. Перед тем, как нить выпустит блокировку, он неизбежно будет чистить значение I обратно в основную память. 2. Видимость: Говоря о видимости, Java использует волатильную, чтобы обеспечить видимость. Когда переменная изменяется летучей, модификация на нее будет немедленно обновлена в основную память. Когда другие потоки должны прочитать переменную, новое значение будет прочитано в памяти. Это не гарантируется обычными переменными. Фактически, синхронизированный и блокировка также могут обеспечить видимость. Перед тем, как поток выпустит блокировку, он пропустит все значения общих переменных обратно в основную память, но синхронизированный и блокировка дороже. 3. Заказ JMM позволяет компилятору и процессору переупорядочить инструкции, но предусматривает семантику AS-IF, то есть независимо от того, насколько переупорядочивание, результат выполнения программы не может быть изменен. Например, следующий сегмент программы:
Double Pi = 3,14; // Aduble r = 1; // bdouble s = pi * r * r; // c
Приведенное выше оператор может быть выполнена в A-> B-> C, причем результат составляет 3.14, но он также может быть выполнен в порядке B-> a-> c. Поскольку A и B являются двумя независимыми утверждениями, в то время как C зависит от A и B, A и B могут быть переупорядочены, но C не может быть ранжирован первым в A и B. JMM гарантирует, что переупорядочение не повлияет на выполнение одного потока, но проблемы склонны к многопоточному. Например, код, как это:
int a = 0; bool flag = false; public void write () {a = 2; // 1 flag = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Если два потока выполняют приведенный выше сегмент кода, поток 1 сначала выполняет записи, то поток 2 затем выполняет умножение, должно ли значение RET 4? Результат не обязательно:
Как показано на рисунке, 1 и 2 в методе записи переупорядочиваются. Поток 1 сначала присваивает флаг true, затем выполняет его в потоке 2, RET непосредственно вычисляет результат, а затем к потоку 1. В настоящее время A назначается 2, что, очевидно, на один шаг спустя. В настоящее время вы можете добавить изменчивое ключевое слово в флаг, запретить переупорядочение, что может обеспечить упорядоченность программы, и вы также можете использовать тяжеловесные синхронизированные и блокировку для обеспечения упорядочения. Они могут гарантировать, что код в этой области выполняется за один раз. Кроме того, у JMM есть некоторый врожденный порядок, то есть упорядоченность, который может быть гарантирован без каких-либо средств, который обычно называется принципом «произойти». << JSR-133: модель памяти Java и спецификация потока >> Определяет следующие правила: 1. Правила последовательности программ: для каждой операции в потоке происходит, потому что раньше используется для любых последующих операций в потоке. Волатильный домен 4. Транзитивность: если происходит A, до B и B,-это то, что правила a a the before c 5.start (): если поток A выполняет операцию threadb_start () (начало потока b), то Threadb_Start () происходит, потому что Arbitrary Operation in an-in trade-in trade-in trade on aepfore on (). потока успешно возвращается из операции ThreatB.join () в потоке A. 7. Принцип Enterrupt (): метод вызовов к потоку enterrupt () возникает сначала, когда событие прерывания обнаруживается прерванным кодом потока. Вы можете использовать метод потока. 8. Принцип завершения (). Первое правило правила последовательности программы гласит, что в потоке все операции находятся в последовательности, но в JMM, до тех пор, пока результат выполнения одинаково, переупорядочение разрешено. Основное внимание происходит, потому что здесь также является правильность результата выполнения с одной меткой, но нельзя гарантировать, что то же самое относится и к многопоточному. Правило 2 Правила монитора на самом деле легко понять. Перед добавлением блокировки вы можете продолжать только добавлять блокировку. Правило 3 применимо к рассматриваемому. Если один поток сначала записывает переменную, а другой поток считывает ее, то операция записи должна быть перед операцией чтения. Четвертое правило-это транзитивность происхождения, прежде чем. Я не буду вдаваться в подробности о следующих немногих.
Затем нам нужно заново упоминать правила летучих переменных: написать летучую домен, прежде чем прочитать этот нестабильный домен позже. Позвольте мне снова вынуть это. На самом деле, если переменная объявляется как летучая, то когда я читаю переменную, я всегда могу прочитать его последнее значение. Здесь последнее значение означает, что независимо от того, какой другой поток записывает переменную, она будет немедленно обновлена в основную память. Я также могу прочитать недавно написанное значение из основной памяти. Другими словами, нестабильное ключевое слово может обеспечить видимость и упорядоченность. Давайте возьмем приведенный выше код в качестве примера:
int a = 0; bool flag = false; public void write () {a = 2; // 1 flag = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Этот код не только обеспокоен переупорядочиванием, даже если 1 и 2 не переупорядочены. 3 тоже не будет выполнено так гладко. Предположим, что поток 1 выполняет операцию записи сначала, а затем поток 2 выполняет операцию умножения. Поскольку поток 1 назначает флаг 1 в рабочей памяти, он не может быть записан обратно в основную память немедленно. Следовательно, когда поток 2 выполняется, умножает значение флага из основной памяти, которая все еще может быть ложным, поэтому операторы в скобках не будут выполнены. Если изменилось на следующее:
int a = 0; volatile bool flag = false; public void write () {a = 2; // 1 flag = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Затем поток 1 выполняет записи сначала, и поток 2 затем выполняет умножение. В соответствии с принципом «Пододеть», этот процесс удовлетворит следующие три типа правил: Правила заказа программы: 1, потому что до 2; 3 случается до 4; (летучие ограничения переупорядочивания инструкций, поэтому 1 выполняется до 2) Правила летучих действий: 2 случаются,-до 3 Правила о переходе: 1 происходит до 4 При написании изменчивой переменной, JMM будет промывать общую переменную в локальной памяти, соответствующую потоку основной памяти. При чтении изменчивой переменной JMM установит локальную память, соответствующую потоку, чтобы недействительна, и поток будет считывать общую переменную из основной памяти.
Прежде всего, мой ответ заключается в том, что атомность не может быть гарантирована. Если это гарантировано, то это только атомичность для чтения/написания одной летучих переменной, но с такими составными операциями не имеет никакого отношения к таким образом, например, к следующему примеру:
Общественный тест класса {public volatile int inc = 0; public void увеличение () {inc ++; } public static void main (string [] args) {окончательный тест теста = new Test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.начинать(); } while (thread.activeCount ()> 1) // Убедитесь, что предыдущие потоки были завершены Thread.yield (); System.out.println (test.inc); }Говоря по логике, результат составляет 10 000, но, вероятно, будет стоимость менее 10 000 при запуске. Некоторые люди могут сказать, что нестабильный не гарантирует видимости. Один поток должен увидеть модификации в INC от INC, а другой поток должен немедленно увидеть это! Но операция INC ++ здесь представляет собой композитную операцию, включая чтение значения Inc, увеличение ее само по себе, а затем записывать ее в основную память. Предположим, что поток A считывает значение INC 10, и в настоящее время он заблокирован, поскольку переменная не модифицирована, а изменчивое правило не может быть инициировано. Поток B также читает значение Inc в настоящее время. Значение INC в основной памяти все еще 10, и оно будет автоматически увеличено, а затем оно будет записано в основную память, которая составляет 11. В настоящее время настала поворот потока A для выполнения. Поскольку 10 сохранен в рабочей памяти, она продолжает расти и записывается в основную память. 11 написано снова. Таким образом, хотя два выполненных потока увеличиваются () дважды, они добавляли только один раз. Некоторые люди говорят, что не изменяет линию кеша? Однако до потока A считывает поток B и выполняет операции, значение INC не изменяется, поэтому при чтении потока B он все еще читает 10. Некоторые люди также говорят, что, если поток B записывает 11 обратно в основную память, не установит линию кэша потока A для аннулирования? Тем не менее, теме A уже выполнила операцию чтения. Только когда операция чтения будет выполнена и линия кэша недействительна, он будет читать основное значение памяти. Следовательно, нить A может продолжать только делать самостоятельно. Подводя итог, в этом роде композитной операции атомная функция не может быть поддержана. Однако в приведенном выше примере установки значения флага, поскольку операция чтения/записи флагов является одноэтапным, он все равно может обеспечить атомичность. Чтобы обеспечить атома, мы можем использовать только синхронизированные, блокировку и атомные классы атомной работы в одновременных пакетах, то есть самостоятельный интрип (добавить 1 операцию), самообедраза (снижение 1 операции), операция добавления (добавление числа) и операции вычитания (вычитайте одно число) основных типов данных, чтобы гарантировать, что эти операции являются атомическими операциями.
Если вы генерируете код сборки с помощью нестабильного ключевого слова и кода без летучего ключевого слова, вы обнаружите, что код с изменчивым ключевым словом будет иметь дополнительную инструкцию префикса блокировки. Инструкция префикса блокировки фактически эквивалентна барьеру памяти. Барьер памяти обеспечивает следующие функции: 1. При повторном порядок следующие инструкции не могут быть переупорядочены в местоположении перед барьером памяти 2. Сделайте кэш этого ЦП, записанного в память ** ** 3. Действие записи также приведет к тому, что другие процессоры или другие ядра для недействительного их кэша, что эквивалентно созданию вновь написанного значения в другие темы.
1. Знак. Количество статуса, как и флаг выше, я упомяну об этом снова:
int a = 0; volatile bool flag = false; public void write () {a = 2; // 1 flag = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Эта операция чтения и записи переменных, помеченная как летучие, может гарантировать, что модификации немедленно видны в потоке. По сравнению с синхронизированным, блокировка имеет определенное повышение эффективности. 2. Реализация режима Синглтона, типичная блокировка двойной проверки (DCL)
Класс Singleton {Private volatile Static Singleton Extance = null; private singleton () {} public static singleton getInstance () {if (exance == null) {synchronized (singleton.class) {if (exant == null) encement = new singleton (); }} return Encement; }}Это ленивый синглтон, объекты создаются только при использовании, и для того, чтобы избежать повторных инструкций для операций инициализации, в экземпляр добавляется волатильный.
Выше приведено все содержание этой статьи об объяснении нестабильных ключевых слов, о которых любят подробно спрашивать интервьюеры Java. Я надеюсь, что это будет полезно для всех. Заинтересованные друзья могут продолжать ссылаться на другие связанные темы на этом сайте. Если есть какие -либо недостатки, пожалуйста, оставьте сообщение, чтобы указать это. Спасибо, друзья, за вашу поддержку на этом сайте!