Volatile можно рассматривать как легковесный Synchronized, который гарантирует видимость только общих переменных. После того как поток A изменит переменную общего доступа, украшенную volatile, поток B сможет прочитать правильное значение. В процессе манипулирования общими переменными в нескольких потоках в java возникнут проблемы переупорядочения инструкций и кеша рабочей памяти общих переменных.
Java-модель памяти
Модель памяти Java требует, чтобы все переменные хранились в основной памяти. Каждый поток также имеет свою рабочую память, и в рабочей памяти потока хранятся переменные, используемые потоком (эти переменные копируются из основной памяти). Все операции (чтение, присваивание) переменных потоком должны выполняться в рабочей памяти. Разные потоки не могут напрямую обращаться к переменным в рабочей памяти друг друга, а передачу значений переменных между потоками нужно делать через основную память.
Три концепции параллельного программирования
видимость
Видимость — сложное свойство, потому что ошибки в видимости всегда противоречат нашей интуиции. В общем случае мы не можем гарантировать, что поток, выполняющий операцию чтения, вовремя увидит значение, записанное другим потоком, а иногда это вообще невозможно. Чтобы обеспечить видимость операций записи в память между несколькими потоками, необходимо использовать механизм синхронизации.
Видимость относится к видимости между потоками, состояние, измененное одним потоком, видно другому потоку. То есть результат модификации потока.
Еще один поток, чтобы увидеть сразу. Например: переменные, измененные с помощью volattitle, будут видны. изменяемая изменяемая переменнаяНе разрешать внутреннее кэширование и переупорядочивание потоков, т.е. прямое изменение памяти. Таким образом, это видно другим потокам. Но здесь есть проблема, на которую стоит обратить внимание, volatile может только сделать видимым свое измененное содержимое, но не может гарантировать его атомарность. Например, volatile int a = 0, после этого следует операция a++, переменная a имеет видимость, но a++ все равно является неатомарной операцией, то есть эта операция тоже имеет проблемы с потокобезопасностью.
Обычные общие переменные не могут гарантировать видимость, потому что после изменения обычных общих переменных неизвестно, когда они будут записаны в основную память.При чтении другими потоками память может все еще иметь старое значение в это время.Поэтому видимость не гарантируется.
Видимость также может быть гарантирована в Java с помощью synchronized и Lock.Synchronized и Lock могут гарантировать, что только один поток получает блокировку одновременно, а затем выполняет код синхронизации, а изменения переменных сбрасываются в основную память перед снятием блокировки. Так что видимость гарантирована.
атомарность
Атом — самая маленькая единица в мире, и он неделим. Атомарность: то есть операция или несколько операций выполняются либо все, и процесс выполнения не прерывается каким-либо фактором, либо ни одна из них не выполняется. В Java операции чтения и присваивания переменным примитивных типов данных являются атомарными операциями, то есть эти операции не прерываемы ни выполняются, ни нет.
Например, a=0; (типы non-long и double) Эта операция неразделима, тогда мы говорим, что эта операция является атомарной операцией. Другой пример: a++; Эта операция на самом деле a = a + 1; делится, так что это не атомарная операция. Неатомарные операции будут иметь проблемы с безопасностью потоков, и нам нужно использовать технологию синхронизации (synchronized), чтобы сделать эту операцию атомарной. Операция атомарна, тогда мы называем ее атомарной. Некоторые атомарные классы предоставляются в параллельном пакете java. Мы можем понять использование этих атомарных классов, прочитав API. Например: AtomicInteger, AtomicLong, AtomicReference и т. д.
Модель памяти Java гарантирует только то, что базовые операции чтения и присваивания являются атомарными операциями.Если вы хотите добиться атомарности более широкого диапазона операций, вы можете использовать для этого синхронизацию и блокировку. Поскольку синхронизация и блокировка могут гарантировать, что только один поток выполняет блок кода в любой момент времени, проблема атомарности, естественно, отсутствует, что обеспечивает атомарность.
упорядоченность
Порядок — это порядок, в котором программа выполняется в порядке, в котором выполняется код.
Что такое переупорядочивание инструкций? Вообще говоря, для повышения эффективности программы процессор может оптимизировать входной код. Это не гарантирует, что порядок выполнения каждого оператора в программе будет таким же, как порядок в коде, но это гарантирует, что программа в конечном итоге будет выполнена.Результат выполнения согласуется с результатом последовательного выполнения кода. Изменение порядка инструкций не повлияет на выполнение отдельного потока, но повлияет на корректность одновременного выполнения потоков. То есть для правильного выполнения параллельных программ должны быть гарантированы атомарность, видимость и упорядоченность. Пока это не гарантировано, программа может вести себя неправильно.В модели памяти Java компилятору и процессору разрешено переупорядочивать инструкции, но процесс переупорядочивания не повлияет на выполнение однопоточных программ, но повлияет на корректность многопоточного параллельного выполнения.
В Java ключевое слово volatile может использоваться для обеспечения определенного «порядка». Кроме того, для обеспечения упорядоченности можно использовать synchronized и Lock.Очевидно, что synchronized и Lock гарантируют, что один поток выполняет код синхронизации в каждый момент времени, что равносильно тому, чтобы потоки выполняли код синхронизации последовательно, что, естественно, обеспечивает упорядоченность.
Кроме того, модели памяти Java присуща некоторая врожденная «упорядоченность», то есть упорядоченность, которую можно гарантировать без каких-либо средств, что часто называют принципом «происходит до». Если порядок выполнения двух операций не может быть выведен из принципа «происходит раньше», то они не могут гарантировать их порядок, и виртуальная машина может изменить их порядок по своему желанию.
изменчивый принцип
Язык java предоставляет несколько более слабый механизм синхронизации, Volatile можно рассматривать как облегченную Synchronized, то есть изменчивую переменную, которая используется для уведомления других потоков об операциях обновления переменных. Когда переменная объявлена volatile, и компилятор, и среда выполнения заметят, что переменная является общей, поэтому операции над переменной не будут переупорядочены с другими операциями с памятью. Изменчивые переменные не кэшируются в регистрах и не невидимы для других процессоров, поэтому чтение изменчивой переменной всегда возвращает самое последнее записанное значение.
При доступе к volatile-переменным операция блокировки не выполняется, поэтому поток выполнения не блокируется, поэтому volatile-переменные представляют собой более легкий механизм синхронизации, чем ключевое слово synchronized.
При чтении или записи в энергонезависимую переменную каждый поток сначала копирует переменную из памяти в кэш ЦП. Если компьютер имеет несколько ЦП, каждый поток может обрабатываться на другом ЦП, что означает, что каждый поток может быть скопирован в другой кэш ЦП.
Объявляя переменную как volatile, JVM гарантирует, что каждая считываемая переменная считывается из памяти, пропуская этап кэширования ЦП.
летучий эффект
1. изменчивая видимость
Как только общая переменная (переменная-член класса, статическая переменная-член класса) изменяется с помощью volatile, она имеет два уровня семантики:
1) Обеспечивает видимость различных потоков, работающих с этой переменной, то есть поток изменяет значение переменной, и новое значение сразу видно другим потокам.
2) Переупорядочивание инструкций запрещено.
//线程1boolean stop = false;while(!stop){ doSomething();}//线程2stop = true;
Этот код является типичным фрагментом кода, и многие люди могут использовать этот метод маркировки при прерывании потока. Но на самом деле, будет ли этот код работать правильно? Ветка будет прервана? Не обязательно, может быть, в большинстве случаев этот код может прервать поток, но он также может привести к прерыванию потока (хотя такая вероятность очень мала, пока это происходит, это вызовет бесконечный цикл).
Объясним, почему этот код может привести к тому, что поток не будет прерван. Как объяснялось ранее, каждый поток во время работы имеет свою собственную рабочую память, поэтому, когда поток 1 выполняется, он копирует значение стоп-переменной и помещает его в свою рабочую память.
Затем, когда поток 2 меняет значение стоповой переменной, но не успел записать в основную память, поток 2 переключается на другие дела, тогда поток 1 продолжит цикл, потому что поток 2 не знает об изменении стопа переменная по потоку 2 идет вниз.
Но после изменения его с помощью volatile он становится другим:
Во-первых: использование ключевого слова volatile приводит к немедленной записи измененного значения в основную память;
Во-вторых: если используется ключевое слово volatile, то при изменении потока 2 строка кеша переменной кеша stop в рабочей памяти потока 1 будет недействительной (если это отражается на аппаратном уровне, это соответствующая строка кеша в потоке 1). Кэш L1 или L2 ЦП).недействительно);
Третье: поскольку строка кеша переменной кеша stop в рабочей памяти потока 1 недействительна, когда поток 1 снова считывает значение переменной stop, она переходит в основную память для чтения.
Затем, когда поток 2 изменяет значение остановки (конечно, здесь есть две операции, изменение значения в рабочей памяти потока 2, а затем запись измененного значения в память), кэш-строка переменной stop будет закэширована в рабочей памяти потока 1 Invalid, затем, когда поток 1 читает, он обнаруживает, что его строка кэша недействительна, он будет ждать обновления адреса основной памяти, соответствующего строке кэша, а затем переходит к соответствующей основной памяти для прочитать последнее значение.
Затем поток 1 считывает последнее правильное значение.
2. Атомарность
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); }}
Каков результат этой программы? Может быть, некоторые друзья думают, что это 10 000. Но на самом деле запуск обнаружит, что результаты каждого запуска несовместимы, все число меньше 10000.
У некоторых друзей могут возникнуть сомнения, нет, вышеизложенное заключается в выполнении операции автоинкремента над переменной inc.Поскольку volatile обеспечивает видимость, после автоинкремента inc в каждом потоке его можно будет увидеть в других потоках.Измененное значение , то есть имеется 10 потоков, выполняющих 1000 операций соответственно, то конечное значение inc должно быть 1000*10=10000.
Здесь есть недоразумение: ключевое слово volatile может гарантировать отсутствие ошибки в видимости, но приведенная выше программа неверна в том смысле, что не может гарантировать атомарность. Видимость может гарантировать только то, что каждое чтение является последним значением, но volatile не может гарантировать атомарность операций над переменными.
Как упоминалось ранее, операция автоинкремента не является атомарной, она включает в себя чтение исходного значения переменной, добавление 1 и запись в рабочую память. Тогда это означает, что три подоперации операции автоинкремента могут выполняться отдельно, что может привести к следующим ситуациям:
Если значение переменной inc в определенный момент равно 10,
Поток 1 выполняет операцию автоматического увеличения переменной.Поток 1 сначала считывает исходное значение переменной inc, а затем поток 1 блокируется;
Затем поток 2 выполняет операцию автоматического увеличения переменной, а поток 2 также считывает исходное значение переменной inc.Поскольку поток 1 только читает переменную inc без изменения переменной, это не приведет к работе потока 2. строка кэша переменной кэша inc в памяти недействительна, и это не приведет к обновлению значения в основной памяти, поэтому поток 2 напрямую перейдет в основную память, чтобы прочитать значение inc, обнаружит, что значение inc равно 10, затем прибавляем к нему 1 и помещаем 11 в рабочую память и, наконец, в основную память.
Затем поток 1 продолжает добавлять 1. Поскольку значение inc было прочитано, обратите внимание, что значение inc в рабочей памяти потока 1 в это время все еще равно 10, поэтому значение inc после того, как поток 1 добавит 1 к inc, равно 11. , затем записывает 11 в рабочую память и, наконец, в основную память.
Затем, после того как два потока выполнят операцию автоинкремента, inc увеличивается только на 1.
Это основная причина.Операция автоинкремента не является атомарной операцией, и volatile не гарантирует, что любая операция над переменной является атомарной.
Решение. Вы можете заблокировать через synchronized или заблокировать, чтобы обеспечить атомарность операции. Также через AtomicInteger.
Некоторые классы атомарных операций предоставляются в пакете java.util.concurrent.atomic java 1.5, то есть самоувеличение (операция добавления 1), самоуменьшение (операция вычитания 1) и операция сложения (добавление числа) к базовые типы данных, операция вычитания (минус число) инкапсулирована, чтобы гарантировать атомарность этих операций. Atomic использует CAS для реализации атомарных операций (Compare And Swap).CAS фактически реализуется с помощью инструкции CMPXCHG, предоставляемой процессором, а процессор, выполняющий инструкцию CMPXCHG, является атомарной операцией.
3. Заказ изменчивых гарантий
Как упоминалось ранее, ключевое слово volatile может запрещать переупорядочивание команд, поэтому volatile может в определенной степени гарантировать порядок.
Ключевое слово volatile, запрещающее переупорядочивание инструкций, имеет два значения:
1) Когда программа выполняет операцию чтения или записи в volatile переменную, все изменения предыдущих операций должны быть выполнены, и результаты должны быть видны последующим операциям; последующие операции не должны выполняться ;
2) При оптимизации инструкций оператор, читающий или записывающий volatile-переменную, не может выполняться после него, а оператор после volatile-переменной не может выполняться перед ним.
//x、y为非volatile变量
//flag为volatile变量
x =
2
;
//语句1
y =
0
;
//语句2
flag =
true
;
//语句3
x =
4
;
//语句4
y = -
1
;
//语句5
Поскольку переменная флага является изменчивой переменной, в процессе переупорядочивания инструкций оператор 3 не будет помещен перед оператором 1 и оператором 2, а оператор 3 не будет помещен после оператора 4 и оператора 5. Обратите внимание, однако, что порядок Заявления 1 и Заявления 2, а также порядок Заявления 4 и Заявления 5 не гарантируется.
А ключевое слово volatile может гарантировать, что при выполнении оператора 3 должны быть выполнены операторы 1 и 2, а результаты выполнения операторов 1 и 2 видны операторам 3, операторам 4 и операторам 5.
//线程1:context = loadContext(); //语句1inited = true; //语句2 //线程2:while(!inited ){ sleep()}doSomethingwithconfig(context);
Принцип реализации изменчивого
Для повышения скорости обработки процессор напрямую не общается с памятью, а считывает данные внутри системы во внутренний кэш и работает, но после операции не знает, когда они будут записаны в память.
Если операция записи выполняется для объявленной volatile-переменной, JVM отправит процессору инструкцию с префиксом Lock для записи данных строки кэша, в которой находится переменная, в системную память. Этот шаг гарантирует, что если другой поток изменит объявленную переменную volatile, данные в основной памяти будут немедленно обновлены.
Однако в настоящее время кэши других процессоров все еще устарели, поэтому в многопроцессорной среде, чтобы обеспечить согласованность кэшей каждого процессора, каждый процесс будет проверять, истек ли срок действия его собственного кэша, анализируя распространяемые данные. на шине. Когда процессор обнаружит, что адрес памяти, соответствующий его строке кэша, был изменен, он установит текущую строку кэша процессора в недопустимое состояние. Когда процессор захочет изменить данные, он будет вынужден повторно считывать данные из системной памяти в кэш процессора. Этот шаг гарантирует, что все объявленные изменчивые переменные, полученные другими потоками, обновлены из основной памяти.
Префиксная инструкция блокировки на самом деле эквивалентна барьеру памяти (также называемому барьером памяти), который гарантирует, что инструкция после нее не будет поставлена в очередь в позицию перед барьером памяти при переупорядочении инструкции, а предыдущая инструкция не будет удалена. поставлена в очередь к барьеру памяти.После, то есть когда инструкция к барьеру памяти выполнена, операции перед ней завершены.
Сценарии применения volatile
Ключевое слово synchronized предназначено для предотвращения одновременного выполнения фрагмента кода несколькими потоками, что сильно повлияет на эффективность выполнения программы, а ключевое слово volatile в некоторых случаях имеет лучшую производительность, чем synchronized, но следует отметить, что ключевое слово volatile не может заменить ключевое слово synchronized. , потому что ключевое слово volatile не может гарантировать атомарность операции. Вообще говоря, использование volatile должно соответствовать следующим двум условиям:
1) Операции записи в переменные не зависят от текущего значения
2) Переменная не входит в инвариант с другими переменными
флаг состояния
Возможно, каноническим использованием реализации volatile-переменных является просто использование логического флага состояния, чтобы указать, что произошло важное одноразовое событие, такое как завершение инициализации или запрос на простоя.
volatile boolean shutdownRequested;
...
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
В процессе выполнения doWork() потоком 1 другой поток 2 может вызвать завершение работы, поэтому логическая переменная должна быть изменчивой.
тогда как если вы используетеsynchronized
Циклы записи блока намного более громоздки, чем запись с изменчивыми флагами состояния. Поскольку volatile упрощает кодирование, а флаги состояния не зависят от какого-либо другого состояния в программе, volatile здесь подходит как нельзя лучше.
Общее свойство этого типа тега состояния:Обычно только один переход состояния;shutdownRequested
логотип отfalse
преобразовать вtrue
, то программа останавливается. Этот режим можно распространить на статусные флаги переходов туда и обратно, но только в том случае, если переходный период незаметен (отfalse
прибытьtrue
, затем преобразовать вfalse
). Кроме того, требуются определенные механизмы перехода атомарного состояния, такие как атомарные переменные.
разовый релиз безопасности
При отсутствии синхронизации можно встретить как обновленное значение ссылки на объект (записанное другим потоком), так и старое значение состояния этого объекта.
Это источник знаменитой проблемы блокировки с двойной проверкой, когда ссылка на объект читается без синхронизации, что создает проблему, заключающуюся в том, что вы можете видеть обновленную ссылку, но все равно проходите. Ссылка видит не полностью построенный объект.
Продолжение следует