Я читал несколько статей о volatile
ключевом слове, но не мог понять, как правильно его использовать. Подскажите, пожалуйста, для чего его использовать в C # и Java?
Я читал несколько статей о volatile
ключевом слове, но не мог понять, как правильно его использовать. Подскажите, пожалуйста, для чего его использовать в C # и Java?
Ответы:
Как для C #, так и для Java "volatile" сообщает компилятору, что значение переменной никогда не должно кэшироваться, поскольку его значение может измениться за пределами области действия самой программы. Тогда компилятор избегает любых оптимизаций, которые могут привести к проблемам, если переменная изменяется «вне его контроля».
Рассмотрим этот пример:
int i = 5;
System.out.println(i);
Компилятор может оптимизировать это, чтобы просто вывести 5, например:
System.out.println(5);
Однако если есть другой поток, который может измениться i
, это неправильное поведение. Если другой поток изменитсяi
на 6, оптимизированная версия все равно будет печатать 5.
volatile
Ключевое слово предотвращает такую оптимизацию и кэширование, и , таким образом , является полезным , когда переменная может быть изменена другим потоком.
i
пометкой volatile
. В Java все сводится к отношениям « произошло до» .
i
это локальная переменная, никакой другой поток не может ее изменить. Если это поле, компилятор не сможет оптимизировать вызов, если это не так final
. Я не думаю, что компилятор может производить оптимизацию, исходя из предположения, что поле «выглядит», final
когда оно явно не объявлено как таковое.
Чтобы понять, что volatile делает с переменной, важно понимать, что происходит, когда переменная не является изменчивой.
Когда два потока A и B обращаются к энергонезависимой переменной, каждый поток будет поддерживать локальную копию переменной в своем локальном кеше. Любые изменения, сделанные потоком A в его локальном кеше, не будут видны потоку B.
Когда переменные объявлены как изменчивые, это по существу означает, что потоки не должны кэшировать такую переменную, или, другими словами, потоки не должны доверять значениям этих переменных, если они не считываются напрямую из основной памяти.
Итак, когда сделать переменную изменчивой?
Когда у вас есть переменная, к которой могут получить доступ многие потоки, и вы хотите, чтобы каждый поток получал последнее обновленное значение этой переменной, даже если значение обновляется любым другим потоком / процессом / вне программы.
Чтения изменчивых полей имеют семантику приобретения . Это означает, что гарантируется, что чтение памяти из изменчивой переменной произойдет до любого следующего чтения из памяти. Он блокирует компилятор от выполнения переупорядочения, и, если аппаратное обеспечение этого требует (слабо упорядоченный ЦП), он будет использовать специальную инструкцию, чтобы заставить оборудование сбрасывать все чтения, которые происходят после изменчивого чтения, но были предположительно запущены раньше, или ЦП мог предотвратить их раннюю выдачу, во-первых, предотвращая возникновение любой спекулятивной нагрузки между выпуском загрузки и ее списанием.
Записи изменчивых полей имеют семантику выпуска . Это означает, что гарантируется, что любая запись в память в изменчивую переменную будет отложена до тех пор, пока все предыдущие записи в память не станут видимыми для других процессоров.
Рассмотрим следующий пример:
something.foo = new Thing();
Если foo
это переменная-член в классе, а другие процессоры имеют доступ к экземпляру объекта, на который ссылается something
, они могут увидеть foo
изменение значения до того, как записи в память в Thing
конструкторе станут глобально видимыми! Вот что означает «слабоупорядоченная память». Это может произойти, даже если у компилятора есть все хранилища в конструкторе перед сохранением в foo
. Если foo
есть, volatile
то хранилище foo
будет иметь семантику выпуска, и оборудование гарантирует, что все записи перед записью foo
будут видны другим процессорам, прежде чем разрешить запись foo
.
Как это возможно, чтобы записи foo
были так плохо переупорядочены? Если кеш-строка foo
хранится в кэше, а хранилища в конструкторе пропустили кеш, то возможно, что хранилище завершится гораздо раньше, чем записи в кеш не пройдут.
(Ужасная) архитектура Itanium от Intel имела слабо упорядоченную память. Процессор, использованный в оригинальном XBox 360, имел слабо упорядоченную память. Многие процессоры ARM, включая очень популярный ARMv7-A, имеют слабо упорядоченную память.
Разработчики часто не видят этих гонок данных, потому что такие вещи, как блокировки, создают полный барьер памяти, по сути то же самое, что и семантика получения и освобождения одновременно. Никакие нагрузки внутри блокировки не могут быть выполнены предположительно до получения блокировки, они откладываются до получения блокировки. Сохранение не может быть отложено при снятии блокировки, инструкция, которая снимает блокировку, откладывается до тех пор, пока все записи, выполненные внутри блокировки, не станут глобально видимыми.
Более полный пример - шаблон «Двойная проверка блокировки». Цель этого шаблона - избежать необходимости всегда получать блокировку для ленивой инициализации объекта.
Позаимствовано из Википедии:
public class MySingleton {
private static object myLock = new object();
private static volatile MySingleton mySingleton = null;
private MySingleton() {
}
public static MySingleton GetInstance() {
if (mySingleton == null) { // 1st check
lock (myLock) {
if (mySingleton == null) { // 2nd (double) check
mySingleton = new MySingleton();
// Write-release semantics are implicitly handled by marking
// mySingleton with 'volatile', which inserts the necessary memory
// barriers between the constructor call and the write to mySingleton.
// The barriers created by the lock are not sufficient because
// the object is made visible before the lock is released.
}
}
}
// The barriers created by the lock are not sufficient because not all threads
// will acquire the lock. A fence for read-acquire semantics is needed between
// the test of mySingleton (above) and the use of its contents. This fence
// is automatically inserted because mySingleton is marked as 'volatile'.
return mySingleton;
}
}
В этом примере хранилища в MySingleton
конструкторе могут быть не видны другим процессорам до сохранения в mySingleton
. Если это произойдет, другие потоки, которые смотрят на mySingleton, не получат блокировку и не обязательно будут получать записи в конструктор.
volatile
никогда не мешает кешированию. Что он делает, так это гарантирует порядок, в котором другие процессоры «видят» записи. Освобождение хранилища будет задерживать сохранение до тех пор, пока все ожидающие записи не будут завершены и не будет запущен цикл шины, сообщающий другим процессорам, что они должны сбросить / записать свою строку кэша, если у них есть кэшированные соответствующие строки. Приобретение нагрузки сбрасывает все предполагаемые чтения, гарантируя, что они не будут устаревшими значениями из прошлого.
head
и другое tail
должно быть изменчивым, чтобы производитель не предполагал, tail
что не изменится, и чтобы потребитель не предполагал, head
что не изменится. Кроме того, он head
должен быть энергозависимым, чтобы гарантировать, что записи данных очереди будут глобально видимы до того, как сохранение head
будет глобально видимым.
Летучее ключевое слово имеет различные значения в обоих Java и C #.
Поле может быть объявлено изменчивым, и в этом случае модель памяти Java гарантирует, что все потоки видят согласованное значение переменной.
Из справочника C # по ключевому слову volatile :
Ключевое слово volatile указывает, что поле в программе может быть изменено чем-то вроде операционной системы, оборудования или одновременно выполняющегося потока.
В Java «volatile» используется, чтобы сообщить JVM, что переменная может использоваться несколькими потоками одновременно, поэтому некоторые общие оптимизации не могут быть применены.
В частности, ситуация, когда два потока, обращающиеся к одной и той же переменной, выполняются на разных процессорах одного и того же компьютера. Процессоры очень часто агрессивно кэшируют данные, которые он хранит, потому что доступ к памяти намного медленнее, чем доступ к кешу. Это означает, что если данные обновляются в CPU1, они должны немедленно пройти через все кеши и в основную память, а не тогда, когда кеш решает очистить себя, чтобы CPU2 мог видеть обновленное значение (опять же, игнорируя все кеши на пути).
Когда вы читаете данные, которые являются энергонезависимыми, выполняющийся поток может получать или не всегда получать обновленное значение. Но если объект изменчив, поток всегда получает самое актуальное значение.
Volatile решает проблему параллелизма. Чтобы это значение синхронизировалось. Это ключевое слово чаще всего используется в потоках. Когда несколько потоков обновляют одну и ту же переменную.