Многие друзья, возможно, слышали о том, что ключевое слово летучие и, возможно, использовали его. До Java 5 это было противоречивое ключевое слово, так как использование его в программах часто приводило к неожиданным результатам. Только после Java 5 летучие ключевые слова восстановили свою жизненную силу.
Хотя нестабильное ключевое слово буквально просто для понимания, его нелегко использовать его хорошо. Поскольку нестабильное ключевое слово связано с моделью памяти Java, прежде чем сообщать нестабильный ключ, мы сначала понимаем концепции и знания, связанные с моделью памяти, затем проанализируем принцип реализации летучего ключевого слова и, наконец, даем несколько сценариев использования нестабильного ключевого слова.
Вот контур каталогов этой статьи:
1. Связанные концепции моделей памяти
Как мы все знаем, когда компьютер выполняет программу, каждая инструкция выполняется в процессоре, а во время выполнения инструкции он неизбежно будет включать чтение и написание данных. Поскольку временные данные во время операции программы хранятся в основной памяти (физическая память), в настоящее время возникает проблема. Поскольку скорость выполнения процессора очень быстрая, процесс чтения данных из памяти и записи данных в память намного медленнее, чем выполнение инструкций ЦП. Следовательно, если операция данных должна быть выполнена посредством взаимодействия с памятью в любое время, скорость выполнения инструкции будет значительно снижена. Следовательно, в процессоре есть кэш.
То есть, когда программа работает, она скопирует данные, требуемые операцией из основной памяти в кеш ЦП. Затем, когда ЦП выполняет расчеты, он может напрямую считывать данные из своего кеша и записывать данные на него. После завершения операции данные в кэше будут промыты в основную память. Давайте приведем простой пример, например, следующий код:
i = i + 1;
Когда поток выполняет этот оператор, он сначала прочтет значение I из основной памяти, затем скопирует копию в кэш, а затем ЦП выполнит инструкцию, чтобы добавить 1 в I, затем записать данные в кэш и, наконец, обновить последнее значение I в кэше в основную память.
Нет проблем с тем, что этот код работает в одном потоке, но при запуске многопоточного. В многоядерных процессорах каждый поток может работать в другом процессоре, поэтому каждый поток имеет свой собственный кэш при запуске (для одноядерных процессоров эта проблема действительно возникнет, но она выполняется отдельно в форме планирования потоков). В этой статье мы принимаем многоядерный процессор в качестве примера.
Например, два потока выполняют этот код одновременно. Если значение I составляет 0 в начале, то мы надеемся, что значение I станет 2 после выполнения двух потоков. Но будет ли это так?
Может быть одна из следующих ситуаций: в начале два потока читают значение I и сохраняют его в кэше их соответствующих процессоров, а затем поток 1 выполняет операцию добавления 1, а затем записывает последнее значение I в память. В настоящее время значение I в кэше потока 2 все еще 0. После выполнения 1 операции значение I равен 1, а затем поток 2 записывает значение I в память.
Значение конечного результата I составляет 1, а не 2. Это знаменитая проблема согласованности кэша. Эта переменная, к которой доступна несколько потоков, обычно называют общей переменной.
То есть, если переменная кэшируется в нескольких процессорах (обычно возникает только во время программирования многопользования), то может возникнуть проблема несоответствия кэша.
Чтобы решить проблему несоответствия кэша, обычно есть два решения:
1) Добавив блокировку# в автобус
2) Через протокол когерентности кэша
Эти два метода предоставляются на уровне аппаратного обеспечения.
В ранних процессорах проблема несоответствия кэша была решена путем добавления замков# в шину. Поскольку связь между процессором и другими компонентами проводится через шину, если шина добавлена с помощью блокировки#, это означает, что другие процессоры блокируются от доступа к другим компонентам (например, памяти), так что только один ЦП может использовать память этой переменной. Например, в приведенном выше примере, если поток выполняет I = I +1, и если сигнал блокировки LCOK# отправляется на шину во время выполнения этого кода, то только после ожидания кода будет полностью выполнен, другие процессоры могут прочитать переменную из памяти, где переменная, которую я находится, и затем выполнить соответствующие операции. Это решает проблему несоответствия кэша.
Но приведенный выше метод будет иметь проблему, потому что другие процессоры не могут получить доступ к памяти во время блокировки шины, что приведет к неэффективности.
Таким образом, появляется протокол согласованности кэша. Самым известным является протокол Intel MESI, который гарантирует, что копия общих переменных, используемых в каждом кэше, является последовательной. Его основная идея: когда ЦП записывает данные, если он обнаруживает, что эксплуатируемая переменная является общей переменной, то есть есть копия переменной в других процессорах, она будет сигнализировать о других процессорах, чтобы установить линию кэша переменной на недопустимое состояние. Следовательно, когда другие процессоры должны прочитать эту переменную и обнаружить, что линия кэша, которая кэширует переменную в их кэше, недействительна, она будет перечитывать из памяти.
2. Три понятия в одновременном программировании
При одновременном программировании мы обычно сталкиваемся с следующими тремя проблемами: проблема атомальности, проблема видимости и упорядоченная проблема. Давайте сначала посмотрим на эти три понятия:
1. Атомность
Атомность: то есть одна операция или несколько операций либо выполняются все, и процесс выполнения не будет прерван какими -либо факторами, либо не будет выполнен.
Очень классический пример - проблема перевода банковского счета:
Например, если вы переводите 1000 юаней из учетной записи A на счет B, он неизбежно будет включать 2 операции: вычтите 1000 юаней из учетной записи A и добавьте 1000 юаней в учетную запись B.
Представьте себе, какие последствия будут вызваны, если эти две операции не будут атомными. Если 1000 юаней вычитается из счета А, операция внезапно будет прекращена. Затем 500 юаней был снят из B, и после снятия 500 юаней, затем операция добавления 1000 юаней к учетной записи B. Это приведет к тому, что, хотя учет A имеет минус 1000 юаней, счета B не получила переведенных 1000 юаней.
Следовательно, эти две операции должны быть атомными, чтобы убедиться, что нет неожиданных проблем.
Какие результаты будут отражены в одновременном программировании?
Чтобы привести самый простой пример, подумайте о том, что произойдет, если процесс назначения 32-разрядной переменной не является атомным?
i = 9;
Если поток выполняет это оператор, я предполагаю, что назначение 32-разрядной переменной включает два процесса: назначение более низкого 16-битного и назначения более высокого 16-битного.
Затем может возникнуть ситуация: когда записано низкое 16-битное значение, оно внезапно прерывается, и в настоящее время другой поток читает значение I, то то, что читается, является неправильным данных.
2. видимость
Видимость относится к тому, когда несколько потоков получают доступ к одной и той же переменной, один поток изменяет значение переменной, а другие потоки могут немедленно увидеть измененное значение.
Для простого примера см. Следующий код:
// Код, выполненный по потоке 1, является int i = 0; i = 10; // код, выполненный по потоке 2, равен j = i;
Если поток выполнения 1 - CPU1, а поток выполнения 2 - CPU2. Из приведенного выше анализа мы видим, что когда поток 1 выполняет предложение I = 10, начальное значение I будет загружено в кэш CPU1, а затем присвоено значение 10. Затем значение I в кэше CPU1 становится 10, но оно не сразу записано в основную память.
В настоящее время поток 2 выполняет j = I, и сначала он перейдет в основную память, чтобы прочитать значение I и загрузить его в кэш CPU2. Обратите внимание, что значение I в памяти все еще 0, поэтому значение j будет 0, а не 10.
Это проблема видимости. После потока 1 изменяется переменная I, поток 2 не сразу видит значение, измененное потоком 1.
3. Заказ
Порядок: то есть порядок выполнения программ выполняется в порядке кода. Для простого примера см. Следующий код:
int i = 0; логический флаг = false; i = 1; // оператор 1 flag = true; // оператор 2
Приведенный выше код определяет переменную типа Int-Type, логическую переменную типа, а затем присваивает значения двум переменным соответственно. С точки зрения последовательности кода, оператор 1 предшествует оператору 2. Поэтому, когда JVM фактически выполняет этот код, убедится ли он, что оператор 1 будет выполнен до оператора 2? Не обязательно, почему? Здесь может произойти повторное порядок инструкций.
Давайте объясним, что такое повторное распоряжение инструкции. Вообще говоря, для повышения эффективности работы программы процессор может оптимизировать входной код. Это не гарантирует, что порядок выполнения каждого оператора в программе соответствовал порядку в коде, но он обеспечит согласованность окончательного результата выполнения программы и результат последовательности выполнения кода.
Например, в приведенном выше коде, который выполняет оператор 1 и оператор 2, сначала не влияет на окончательный результат программы, тогда возможно, что во время процесса выполнения оператор 2 выполняется первым, а оператор 1 выполняется позже.
Но имейте в виду, что, хотя процессор перезарядит инструкции, он гарантирует, что конечный результат программы будет таким же, как последовательность выполнения кода. Так что же это гарантирует? Давайте посмотрим на следующий пример:
int a = 10; // утверждение 1int r = 2; // оператор 2a = a + 3; // утверждение 3r = a*a; // оператор 4
Этот код имеет 4 оператора, поэтому возможный заказ на выполнение:
Так возможно ли быть заказом выполнения: оператор 2 оператора 1 оператор 4 оператора 4 оператора 3
Это невозможно, потому что процессор рассмотрит зависимость данных между инструкциями при переупорядочке. Если инструкция по инструкции 2 должна использовать результат инструкции 1, процессор обеспечит выполнение инструкции 1 до инструкции 2.
Хотя переупорядочение не повлияет на результаты выполнения программы в пределах одного потока, как насчет многопоточного чтения? Давайте посмотрим пример ниже:
// поток 1: context = LoadContext (); // состояние 1Inited = true; // состояние 2 // потока 2: while (! Inited) {sleep ()} dosomethingwithconfig (context);В приведенном выше коде, поскольку утверждения 1 и 2 не имеют никаких зависимостей данных, они могут быть переупорядочены. Если произойдет переупорядочение, оператор 2 сначала выполняется во время выполнения потока 1, и это поток 2 будет думать, что работа по инициализации была завершена, а затем он выпрыгнет из цикла, чтобы выполнить метод DoSomThingWithConfig (контекст). В настоящее время контекст не инициализируется, что приведет к ошибке программы.
Как видно из вышеперечисленного, повторное порядок инструкций не повлияет на выполнение одного потока, но повлияет на правильность одновременного выполнения потоков.
Другими словами, для правильного выполнения параллельных программ необходимо обеспечить атомность, видимость и упорядоченность. Пока кто -то не гарантируется, это может привести к неправильному запуску программы.
3. Модель памяти джава
Я говорил о некоторых проблемах, которые могут возникнуть в моделях памяти и одновременном программировании. Давайте посмотрим на модель памяти Java и изучим, что гарантирует, что модель памяти Java предоставляет нам, и какие методы и механизмы предоставляются в Java, чтобы обеспечить правильность выполнения программы при выполнении многопоточного программирования.
В спецификации виртуальной машины Java предпринимается попытка определить модель памяти Java (JMM), чтобы заблокировать различия доступа к памяти между различными аппаратными платформами и операционными системами, чтобы позволить программам Java для достижения постоянных эффектов доступа к памяти на различных платформах. Так что же предусматривает модель памяти Java? Он определяет правила доступа для переменных в программе. Чтобы выразить это более широким, это определяет порядок выполнения программы. Обратите внимание, что для того, чтобы получить лучшую производительность выполнения, модель памяти Java не ограничивает механизм выполнения использования регистров или кэша процессора для улучшения скорости выполнения инструкций, а также не ограничивает компилятор для повторного порядка инструкций. Другими словами, в модели памяти Java также будут проблемы с согласованностью кэша и проблемы с повторным заказом инструкций.
Модель памяти Java предусматривает, что все переменные находятся в основной памяти (аналогично физической памяти, упомянутой выше), и в каждом потоке есть своя собственная рабочая память (аналогично предыдущему кешу). Все операции потока на переменной должны выполняться в рабочей памяти и не могут напрямую работать в основной памяти. И каждый поток не может получить доступ к рабочей памяти других потоков.
Чтобы привести простой пример: в Java выполните следующее оператор:
i = 10;
Поток выполнения должен сначала назначить линию кэша, где переменная, которую я находится в своем собственном рабочем потоке, а затем написать ее в основную память. Вместо того, чтобы писать значение 10 непосредственно в основную память.
Итак, какие гарантии сама язык Java обеспечивает атомность, видимость и упорядоченность?
1. Атомность
В Java операциями чтения и назначения переменных основных типов данных являются атомные операции, то есть эти операции не могут быть прерваны и выполнены или не выполнены или нет.
Хотя вышеупомянутое предложение кажется простым, оно не так просто для понимания. Смотрите следующий пример i:
Пожалуйста, проанализируйте, какие из следующих операций являются атомными операциями:
x = 10; // оператор 1y = x; // оператор 2x ++; // оператор 3x = x + 1; // оператор 4
На первый взгляд, некоторые друзья могут сказать, что операции в вышеупомянутых четырех утверждениях являются атомными операциями. Фактически, только утверждение 1 является атомной операцией, и ни одно из трех других утверждений не является атомным операциями.
Оператор 1 напрямую присваивает значение от 10 до x, что означает, что поток выполняет это оператор и записывает значение 10 непосредственно в рабочую память.
Заявление 2 фактически содержит 2 операции. Сначала необходимо прочитать значение x, а затем написать значение x в рабочую память. Хотя две операции чтения значения x и написания значения X в рабочей памяти являются атомными операциями, они не являются атомными операциями вместе.
Точно так же x ++ и x = x+1 включают 3 операции: прочитайте значение x, выполните операцию добавления 1 и напишите новое значение.
Следовательно, только операция оператора 1 в вышеупомянутых четырех утверждениях является атомной.
Другими словами, только простое чтение и назначение (и число должно быть назначено переменной, а взаимное назначение между переменными не является атомной операцией) является атомной операцией.
Тем не менее, здесь есть одна вещь: под 32-разрядной платформой, чтение и назначение 64-битных данных должны быть выполнены через две операции, и его атомность не может быть гарантирована. Тем не менее, кажется, что в последнем JDK JVM гарантировал, что чтение и назначение 64-битных данных также является атомной работой.
Из вышесказанного видно, что модель памяти Java гарантирует, что основные чтения и назначения являются атомными операциями. Если вы хотите достичь атомичности большего диапазона операций, это может быть достигнуто с помощью синхронизации и блокировки. Поскольку синхронизированный и блокировка может гарантировать, что только один поток выполняет кодовый блок в любое время, естественно не будет проблем с атомацией, что обеспечивает атома.
2. видимость
Для видимости Java предоставляет нестабильное ключевое слово для обеспечения видимости.
Когда общая переменная изменяется летучейю часть, она гарантирует, что измененное значение будет обновлено до основной памяти немедленно, и когда другие потоки должны его прочитать, оно будет читать новое значение в памяти.
Тем не менее, обычные общие переменные не могут гарантировать видимость, потому что это неясно, когда нормальная общая переменная записывается в основную память после ее изменения. Когда другие потоки читают его, исходное старое значение все еще может быть в памяти, поэтому видимость не может быть гарантирована.
Кроме того, синхронизированный и блокировка также могут обеспечить видимость. Синхронизированный и блокировка может убедиться, что только один поток получает блокировку одновременно и выполняет код синхронизации. Прежде чем выпустить блокировку, модификация переменной будет обновлена в основную память. Поэтому видимость может быть гарантирована.
3. Заказ
В модели памяти Java компиляторам и процессорам разрешается переупорядочить инструкции, но процесс повторного порядка не повлияет на выполнение однопоточных программ, но повлияет на правильность многопоточного одновременного выполнения.
В Java определенная «линия порядка» может быть обеспечена с помощью летучих ключевых слов (конкретный принцип объясняется в следующем разделе). Кроме того, синхронизированный и блокировка могут быть использованы для обеспечения порядка. Очевидно, что синхронизированный и блокировка убедитесь, что существует поток, который выполняет код синхронизации в каждый момент, что эквивалентно разрешению потоков выполнять код синхронизации в последовательности, что естественным образом обеспечивает порядок.
Кроме того, модель памяти Java имеет некоторую врожденную «Lines Line», то есть ее можно гарантировать без каких-либо средств, что обычно называется принципом «произойти». Если порядок выполнения двух операций не может быть получен из принципа «произойти», то они не могут гарантировать их упорядоченность, и виртуальные машины могут переупорядочить их по желанию.
Давайте представим принцип «Приоритетный приоритет» (принцип приоритета):
Эти 8 принципов выхищены из «глубокого понимания виртуальных машин Java».
Среди этих 8 правил первые 4 правила более важны, в то время как все последние 4 правила очевидны.
Давайте объясним первые 4 правила ниже:
Для правил заказа программы, насколько я понимаю, состоит в том, что выполнение кода программы, по -видимому, заказано в одном потоке. Обратите внимание, что, хотя в этом правиле упоминается, что «операция, написанная на передней части, происходит сначала в операции, написанной на спине», это должен быть порядок, в котором программа, по -видимому, выполняется в последовательности кода, поскольку виртуальная машина может изменить порядок программы. Несмотря на то, что переупорядочение выполняется, результат окончательного выполнения согласуется с последовательным выполнением программы, и он будет только переупорядочивать инструкции, которые не имеют зависимости данных. Следовательно, в одном потоке выполнение программы, по -видимому, выполняется упорядоченным образом, что следует понимать с осторожностью. Фактически, это правило используется для обеспечения правильности результатов выполнения программы в одном потоке, но оно не может гарантировать правильность программы многопоточным образом.
Второе правило также легче понять, то есть, если тот же замок находится в заблокированном состоянии, оно должно быть опубликовано до того, как операция блокировки будет продолжена.
Третье правило является относительно важным правилом, а также является то, что будет обсуждаться позже. Интуитивно, если поток сначала записывает переменную, а затем считывает поток, то операция записи определенно будет происходить сначала в операции чтения.
Четвертое правило на самом деле отражает, что принцип «случается», прежде чем транзитивен.
4. углубленный анализ летучих ключевых слов
Я говорил о многих вещах раньше, но на самом деле они прокладывают способ сказать изменчивое ключевое слово, поэтому давайте перейдем к теме.
1. Двухслойная семантика нестабильных ключевых слов
После того, как общая переменная (переменные члена класса, класс статические переменные элемента) изменяется летучей, она имеет два уровня семантики:
1) Обеспечить видимость различных потоков при использовании этой переменной, то есть один поток изменяет значение определенной переменной, и это новое значение немедленно видно для других потоков.
2) Запрещено переориентировать инструкции.
Давайте сначала посмотрим на кусок кода. Если поток 1 выполняется первым, а поток 2 выполняется позже:
// потока 1Boolean Stop = false; while (! Stop) {dosomething ();} // потока 2stop = true;Этот код является очень типичным куском кода, и многие люди могут использовать этот метод разметки при прерывании потоков. Но на самом деле этот код работает полностью правильно? Будет ли поток прерван? Не обязательно. Возможно, большую часть времени этот код может прерывать потоки, но он также может привести к прерыванию поток (хотя эта возможность очень мала, как только это произойдет, это приведет к мертвому петлю).
Давайте объясним, почему этот код может привести к тому, что поток не прервел. Как объяснялось ранее, каждый поток имеет свою собственную рабочую память во время работы, поэтому при запуске потока 1 он скопирует значение переменной Stop и помещает ее в свою собственную рабочую память.
Затем, когда поток 2 изменяет значение переменной остановки, но у меня не было времени, чтобы написать ее в основную память, поток 2 делает другие вещи, тогда поток 1 не знает об изменениях потока 2 в переменной остановки, поэтому он будет продолжать цикл.
Но после изменения с летучим он становится другим:
Во -первых: использование летучих ключевых слов заставит немедленно записано в основную память в основную память;
Во -вторых: если вы используете летучие ключевое слово, когда поток 2 изменяет его, линия кэша остановки переменной кэша в рабочей памяти потока 1 будет недействительной (если она отражена в аппаратном слое, соответствующая линия кэша в кэше L1 или L2 в CPU является недостаточной);
ТРЕТЬЯ: Поскольку строка кэша остановки переменной кэша в рабочей памяти потока 1 недействительна, поток 1 будет считывать ее в основной памяти, когда она снова считывает значение остановки переменной.
Затем, когда поток 2 изменяет значение остановки (конечно, здесь есть 2 операции, изменяя значение в рабочей памяти потока 2, а затем написание модифицированного значения в память), строка кэша остановки переменной кэша в рабочей памяти потока 1 будет недействительной. Когда нить 1 читается, он обнаруживает, что его линия кэша недействительна. Он будет ждать обновления соответствующего основного адреса памяти линии кэша, а затем прочтет последнее значение в соответствующей основной памяти.
Затем то, что считывает поток 1, является последним правильным значением.
2. Волатильная гарантия атомства?
Из вышесказанного мы знаем, что летучие ключевые слова обеспечивает видимость операций, но может летучие гарантировать, что операции на переменных являются атомными?
Давайте посмотрим пример ниже:
Общественный тест класса {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); }}Подумайте о том, каков результат вывода этой программы? Может быть, некоторые друзья думают, что это 10000. Но на самом деле, запуск его обнаружит, что результаты каждого прогона непоследовательны, и это число менее 10 000.
У некоторых друзей могут быть вопросы, это неправильно. Выше представлено для выполнения операции самостоятельной инккра в переменной вкл. Поскольку летучая часть обеспечивает видимость, после самостоятельного вступления в ИнК в каждом потоке модифицированное значение можно увидеть в других потоках. Следовательно, 10 потоков выполнили 1000 операций соответственно, поэтому конечное значение INC должно быть 1000*10 = 10000.
Здесь есть недоразумение. Ключевое слово «летучих» может обеспечить видимость, но вышеуказанная программа неверна, потому что оно не может гарантировать атомность. Видимость может только гарантировать, что последнее значение читается каждый раз, но нестабильное не может гарантировать атомацию работы переменных.
Как упоминалось ранее, операция автоматического размещения не является атомной. Он включает в себя чтение исходного значения переменной, выполнение дополнительной операции и написание рабочей памяти. То есть три подъездных операции операции самостоятельного достижения могут быть выполнены отдельно, что может привести к следующей ситуации:
Если значение переменной INC в определенное время составляет 10,
Поток 1 выполняет операцию самостоятельного достижения на переменной. Поток 1 сначала считывает исходное значение переменной Inc, а затем поток 1 блокируется;
Затем поток 2 выполняет операцию самостоятельной инккра с переменной, а поток 2 также считывает исходное значение переменной вкл. Поскольку поток 1 выполняет только операцию чтения на переменной INC и не изменяет переменную, он не приведет к недействительной линии кэша переменной Cache Inc Cache Inc в потоке 2. Поэтому поток 2 будет напрямую перейти к основной памяти, чтобы прочитать значение INC. Когда обнаруживается, что значение INC составляет 10, затем выполняет операцию добавления 1 и записывает 11 в рабочую память и, наконец, записывает ее в основную память.
Затем резьба 1 затем выполняет операцию добавления. Поскольку значение INC было прочитано, обратите внимание, что значение INC в потоке 1 по -прежнему 10 в настоящее время, поэтому после потока 1 добавлен Inc, значение INC составляет 11, затем пишет 11 для рабочей памяти и, наконец, записывает ее в основную память.
Затем после того, как два потока выполняют операцию самостоятельной реализации, INC увеличивается только на 1.
Объяснив это, у некоторых друзей могут быть вопросы, это неправильно. Разве это не гарантировано, что переменная будет аннулировать линию кэша при изменении летучих переменной? Тогда другие потоки будут читать новое значение. Да, это правильно. Это правило летучих переменных в правиле, прежде всего, выше, но следует отметить, что если поток 1 считывает переменную и блокируется, значение INC не будет изменено. Затем, хотя летучие может гарантировать, что поток 2 считывает значение переменной INC из памяти, поток 1 не изменил его, поэтому поток 2 вообще не увидит модифицированное значение вообще.
Корневая причина заключается в том, что операция автоинсюрмана не является атомной работой, и нестабильная не может гарантировать, что любая операция на переменных является атомальной.
Измените приведенный выше код на любое из следующего, может достичь эффекта:
Используйте синхронизированный:
Общественный класс тест {public int inc = 0; публичное синхронизированное увеличение пустоты () {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); }} Используя блокировку:
Общественный класс тест {public int inc = 0; Lock lock = new Reentrantlock (); public void увеличение () {lock.lock (); попробуйте {inc ++; } наконец {lock.unlock (); }} 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 (); }; }.начинать(); } while (thread.activeCount ()> 1) // Убедитесь, что предыдущие потоки были выполнены Tread.yield (); System.out.println (test.inc); }} Использование AtomicInteger:
Общественный тест класса {public atomicinteger inc = new Atomicinteger (); public void увеличение () {inc.getandIncrement (); } 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) // Убедитесь, что предыдущие потоки были выполнены Tread.yield (); System.out.println (test.inc); }}Некоторые классы атомной операции представлены в рамках java.util.concurrent.atomic пакета Java 1.5, а именно, самостоятельно интриптом (добавление 1 операции), самообедка (добавить 1 операцию), операция добавления (добавить число) и операции вычитания (добавить число) основных типов данных, чтобы гарантировать, что эти операции являются атомическими операциями. Atomic использует CAS для реализации атомных операций (сравнение и обмена). CAS фактически реализуется с использованием инструкций CMPXCHG, предоставленных процессором, а процессор выполняет инструкции CMPXCHG, является атомной операцией.
3. может быть летучим, обеспечить упорядоченность?
Как упоминалось ранее, нестабильное ключевое слово может запретить переупорядочение инструкций, поэтому летучие могут обеспечить порядок в определенной степени.
Есть два значения запрещенных переупорядочения изменчивых ключевых слов:
1) Когда программа выполняет операцию чтения или записи летучей переменной, должны были быть сделаны все изменения в предыдущих операциях, и результат уже видна последующим операциям; Последующие операции, должно быть, еще не были сделаны;
2) При выполнении оптимизации инструкций оператор, доступный к летучей переменной, не может быть помещен за ним, и при этом операторы, следующие за изменчивой переменной, не могут быть размещены перед ней.
Может быть, то, что сказано выше, немного сбивает с толку, поэтому приведите простой пример:
// x и y являются нелетучими переменными // флаг является летучей переменной x = 2; // оператор 1y = 0; // утверждение 2flag = true; // оператор 3x = 4; // утверждение 4y = -1; // оператор 5
Поскольку переменная флага является летучей переменной, при выполнении процесса переупорядочения инструкции, оператор 3 не будет размещен до оператора 1 и 2, а также не будет размещено после оператора 3 и оператора 4 и 5. Однако не гарантируется, что Орден оператора 1 и оператор 2 и оператор 4 и оператор 5 и оператор 5 не гарантированы.
Кроме того, изменчивое ключевое слово может убедиться, что при выполнении оператора 3 должно быть выполнено оператор 1 и оператор 2, а результаты выполнения оператора 1 и оператора 2 видны с оператором 3, оператором 4 и оператором 5.
Итак, давайте вернемся к предыдущему примеру:
// поток 1: context = LoadContext (); // состояние 1Inited = true; // состояние 2 // потока 2: while (! Inited) {sleep ()} dosomethingwithconfig (context);Когда я дал этот пример, я упомянул, что возможно, что утверждение 2 будет выполнено до оператора 1, пока он может привести к тому, что контекст не будет инициализирован, а в потоке 2 используется ненициализированный контекст для работы, что приведет к ошибке программы.
Если сглаживаемая переменная изменена с помощью нестабильного ключевого слова, эта проблема не возникнет, потому что, когда оператор 2 будет выполнен, она определенно обеспечит инициализирован контекст.
4. Принцип и механизм реализации летучих
Предыдущее описание некоторого использования летучих ключевых слов произошло от. Let’s discuss how volatile ensures visibility and prohibits instructions to reorder.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;} volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; }}Ссылки:
《Java编程思想》
《深入理解Java虚拟机》
Выше всего содержание этой статьи. Я надеюсь, что это будет полезно для каждого обучения, и я надеюсь, что все будут поддерживать Wulin.com больше.