Java volatile что это: Зачем нужно volatile в Java — urvanov.ru

Содержание

Зачем нужно volatile в Java — urvanov.ru

Многие разработчики Java, даже те, которые пишут на нём уже несколько лет, не знают, что делает ключевое слово volatile в Java.

Пример поля с volatile:

class A { private volatile double someField1; }

class A {

    private volatile double someField1;

}

Использование volatile — это один из способов обеспечения согласованного доступа к переменной разными потоками. Более подробно о разработке многопоточных приложений в Java и синхронизации между потоками можно прочесть в моей статье про многопоточность в Java.

Ключевое слово volatile указывается для поля для того, чтобы указать компилятору, что все операции присвоения этой переменной и все операции чтения из неё должны быть атомарными.

Более того, присвоение значения этой переменной имеет связь happens-before (произошло-до) для последующих чтений из этой переменной для любых потоков, то есть после присвоения нового значения переменной все потоки увидят это новое значение. Дело в том, что Java позволяет потокам в целях производительности сохранять локальные копии переменной для каждого потока, который её использует (например в кешах или регистрах процессора). В таком случае после записи другим потоком нового значения в исходную переменную, первый поток будет видеть свою локальную копию со старым значением. Использование ключевого слова volatile гарантирует, что все потоки всегда будут использовать общее, исходное значение, и они будут видеть изменения этого исходного значения другими потоками сразу же. Аналогично все изменения переменных, произошедшие внутри sychronized-методов и synchronized-блоков, а также блоков с другими блокировками вроде реализаций интерфейса java.util.concurrent.locks.Lock после выхода из блокировки будут гарантировано видны любым другим потокам после взятия блокировки над тем же самым объектом, но если более сложные блокировки не нужны, то можно использовать volatile. Можно даже использовать их совместно — особого вреда, я думаю, не будет.

Переменная volatile  используется в одном из вариантов реализаций паттерна синглетон.


Поделиться:

5 вещей, которых вы не знали о многопоточности / Хабр

Хоть от многопоточности и библиотек, которые её поддерживают, отказываются немногие Java-программисты, но тех, кто нашёл время изучить вопрос в глубину ещё меньше. Вместо этого мы узнаём о потоках только столько, сколько нам требуется для конкретной задачи, добавляя новые приёмы в свой инструментарий лишь тогда, когда это необходимо. Так можно создавать и запускать достойные приложения, но можно делать и лучше. Понимание особенностей компилятора и виртуальной машины Java поможет вам писать более эффективный, производительный код.

В этом выпуске серии «5 вещей …», я представлю некоторые из тонких аспектов многопоточного программирования, в том числе synchronized-методы, volatile переменные и атомарные классы. Речь пойдет в особенности о том, как некоторые из этих конструкций взаимодействуют с JVM и Java-компилятором, и как различные взаимодействия могут повлиять на производительность приложений.

Примечание переводчика: я как раз из тех людей, которые не знали этих пяти вещей о многопоточном программировании, поэтому посчитала, что эта статья стоит того, чтобы её обнародовать здесь, но и поэтому же могла допустить некоторые ошибки в переводе, так что поправки приветствуются с энтузиазмом.
Примечание переводчика2: в комментариях знающие люди делятся ссылками и информацией по теме, не менее интересными, чем содержание статьи)

1. Synchronized-метод или synchronized-блок?

Вы, возможно, уже задумывались о том, объявлять ли синхронизированным весь метод или только ту его часть, которую необходимо обезопасить. В таких ситуациях, полезно знать, что когда компилятор Java преобразует исходный код в байт-код, он работает с synchronized-методами и synchronized-блоками очень по-разному.

Когда JVM выполняет synchronized-метод, выполняющийся поток определяет, что в method_info этого метода проставлен флаг ACC_SYNCHRONIZED. Тогда он автоматически устанавливает блокировку на объект, вызывает метод и снимает блокировку. Если вылетает исключение, поток автоматически снимает блокировку.
С другой стороны, synchronized-блок обходит встроенную в JVM поддержку запросов блокировок объекта и обработку исключений, так что это необходимо описывать явно в байт-коде. Если вы посмотрите на байт-код для блока, увидите в нём кучу дополнительных операций в сравнении с методом. Листинг 1 показывает вызов и того, и другого.

Листинг 1. Два подхода к синхронизации.

package com.geekcap;
public class SynchronizationExample {
    private int i;
 
    public synchronized int synchronizedMethodGet() {
        return i;
    }
 
    public int synchronizedBlockGet() {
        synchronized( this ) {
            return i;
        }
    }
}

Метод synchronizedMethodGet() method генерирует следующий байт-код:
	0:	aload_0
	1:	getfield
	2:	nop
	3:	iconst_m1
	4:	ireturn

А вот байт-код для метода synchronizedBlockGet():
        0:	aload_0
	1:	dup
	2:	astore_1
	3:	monitorenter
	4:	aload_0
	5:	getfield
	6:	nop
	7:	iconst_m1
	8:	aload_1
	9:	monitorexit
	10:	ireturn
	11:	astore_2
	12:	aload_1
	13:	monitorexit
	14:	aload_2
	15:	athrow

Создание synchronized-блока выдало 16 строк байт-кода, тогда как synchronized-метода – только 5.
2. «Внутрипоточные» (ThreadLocal) переменные.

Если вы хотите сохранить один экземпляр переменной для всех экземпляров класса, вы используете статические переменные класса. Если вы хотите сохранить экземпляр переменной для каждого потока, используйте внутрипоточные (ThreadLocal) переменные. ThreadLocal переменные отличаются от обычных переменных тем, что у каждого потока свой собственный, индивидуально инициализируемый экземпляр переменной, доступ к которой он получает через методы get() или set().

Предположим, вы разрабатываете многопоточный трассировщик кода, чьей целью является однозначное определение пути каждого потока через ваш код. Проблема в том, что вам необходимо скоординировать несколько методов в нескольких классах через несколько потоков. Без ThreadLocal это было бы трудноразрешимо. Когда поток начинал бы выполняться, было бы необходимо сгенерировать уникальный маркер для идентификации его трассировщиком, а потом передавать этот маркер каждому методу при трассировке.

С ThreadLocal это проще. Поток инициализирует ThreadLocal переменную в начале выполнения, а затем обращается к нему из каждого метода в каждом классе, и переменная при этом будет хранить трассировочную информацию только для исполняемого в данный момент времени потока. Когда его выполнение завершится, поток может передать свою индивидуальную запись о трассировке объекту управления, ответственному за поддержание всех записей.

Использование ThreadLocal имеет смысл, когда вам необходимо хранить экземпляры переменной для каждого потока.

3. Volatile переменные.

По моим оценкам, лишь половина всех разработчиков Java знает, что в Java есть ключевое слово volatile. Из них лишь около 10 процентов знают, что оно значит, и еще меньше знают, как эффективно его использовать. Короче говоря, определение переменной с ключевым словом volatile(«изменчивый») означает, что значение переменной будет изменяться разными потоками. Чтобы полностью понять, что значит volatile, во-первых, нужно понять, как потоки оперируют с обычными, не-volatile, переменными.

В целях повышения эффективности работы, спецификации языка Java позволяет JRE сохранять локальную копию переменной в каждом потоке, который ссылается на нее. Можно считать эти «внутрипоточные» копии переменных похожими на кэш, помогающий избежать проверки главной памяти каждый раз, когда требуется доступ к значению переменной.

Но представьте, что произойдёт в следующем случае: запустятся два потока, и первый прочитает переменную А как 5, тогда как второй – как 10. Если переменная А изменились от 5 до 10, то первый поток не будет знать об изменении, так что будет иметь неправильное значение А. Однако если переменная А будет помечена как volatile, то то в любое время, когда поток обращается к её значению, он будет получать копию А и считывать её текущее значение.

Если переменные в вашем приложении не меняются, то внутрипоточный кэш имеет смысл. В противном случае, очень полезно знать, что может сделать для вас ключевое слово volatile.

4. Volatile против synchronized.

Если переменная объявлена как volatile, это означает, что ожидается её изменение несколькими потоками. Естественно, вы думаете, что JRE наложит какие-то формы синхронизации для volatile переменных. Хорошо это или плохо, JRE неявно обеспечивает синхронизацию при доступе к volatile переменным, но с одной очень большой оговоркой: чтение volatile переменных синхронизировано и запись в volatile переменные синхронизирована, а неатомарные операции – нет.
Что означает, что следующий код не безопасен для потоков:
myVolatileVar++;

Этот код также может быть записан следующим образом:
int temp = 0;
synchronize( myVolatileVar ) {
  temp = myVolatileVar;
}
 
temp++;
 
synchronize( myVolatileVar ) {
  myVolatileVar = temp;
}
 

Другими словами, если volatile переменная обновляется неявно, то есть значение читается, измененяется, а затем присваивается как новое, результат будет не-потокобезопасным между двумя синхронными операциями. Вы можете выбирать, следует ли использовать синхронизацию или рассчитывать на поддержку JRE автоматической синхронизации volatile переменных. Наилучший подход зависит от вашего случая: если присвоенное значение volatile переменной зависит от её текущего значения (например, во время операции инкремента), то нужно использовать синхронизацию, если вы хотите, чтобы операция была потокобезопасной.
5. Обновления атомарных полей.

Когда вам требуется примитивный тип, выполняющий операции инкремента и декремента, гораздо лучше выбрать его среди новых атомарных классов в пакете java.util.concurrent.atomic, чем писать synchronized блок самому. Атомарные классы гарантируют, что определённые операции будут выполняться потокобезопасно, например операции инкремента и декремента, обновления и добавления(add) значения. Список атомных классов включает AtomicInteger, AtomicBoolean, AtomicLong, AtomicIntegerArray, и так далее.

Своеобразным вызовом программисту в использовании атомарных классов является то, что все операции класса, включая get, set и семейство операций get-set тоже атомарные. Это значит, что операции чтения и записи, которые не изменяют значения атомарной переменной, синхронизированы, а не только важные операции чтения-обновления-записи. Если вы хотите более детального контроля над развертыванием синхронизированного кода, то обходной путь заключается в использовании атомарного апдейтера поля.

Использование атомарного апдейтера.

Атомарные апдейтеры типа AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, и AtomicReferenceFieldUpdater по существу оболочки применяющиеся к volatile полям. Внутри, библиотеки классов Java используют их. Хотя они не часто используются в коде приложений, но у вас нет причин не начать облегчать свою жизнь с их помощью.

Листинг 2 демонстрирует пример класса, который использует атомарные обновления для изменения книги, которую кто-то читает:
Листинг 2. Класс Book.

package com.geeckap.atomicexample;
 
public class Book
{
    private String name;
 
    public Book()
    {
    }
 
    public Book( String name )
    {
        this.name = name;
    }
 
    public String getName()
    {
        return name;
    }
 
    public void setName( String name )
    {
        this.name = name;
    }
}
 

Класс Book – просто POJO (plain old Java object – незамысловатый старый Java объект), у которого есть только одно поле: name.

Листинг 3. Класс MyObject.

package com.geeckap.atomicexample;
 
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
 
/**
 *
 * @author shaines
 */
public class MyObject
{
    private volatile Book whatImReading;
 
    private static final AtomicReferenceFieldUpdater<MyObject,Book> updater =
            AtomicReferenceFieldUpdater.newUpdater( 
                       MyObject.class, Book.class, «whatImReading» );
 
    public Book getWhatImReading()
    {
        return whatImReading;
    }
 
    public void setWhatImReading( Book whatImReading )
    {
        //this.whatImReading = whatImReading;
        updater.compareAndSet( this, this.whatImReading, whatImReading );
    }
}
 

Класс MyObject в листинге 3 представляет, как и можно было ожидать, get и set методы, но метод set делает кое-что иное. Вместо того, чтобы просто предоставить свою внутреннюю ссылку на указанную книгу (что было бы выполнено закомментированным кодом в листинге 3), он использует AtomicReferenceFieldUpdater.
AtomicReferenceFieldUpdater

Javadoc определяет AtomicReferenceFieldUpdater так:

A reflection-based utility that enables atomic updates to designated volatile reference fields of designated classes. This class is designed for use in atomic data structures in which several reference fields of the same node are independently subject to atomic updates.
(Основанная на отражении утилита, которая разрешает атомарные обновления назначенным volatile ссылочным полям назначенных классов. Этот класс предназначен для использования в атомарных структурах данных, в которых несколько ссылочных полей одной и той же записи являются независимыми субъектами для атомарных обновлений)убейте меня, я не знаю, как это нормально перевести

В листинге 3 AtomicReferenceFieldUpdater создан через вызов метода newUpdater, который принимает три параметра.
• класс объекта, содержащего поле (в данном случае, MyObject)
• класс объекта, который будет обновляться атомарно (в данном случае, Book)
• имя поля для атомарного обновления

Значимым здесь является то, что метод getWhatImReading выполняется без синхронизации любого рода, в то время как setWhatImReading выполняется как атомарная операция.

В листинге 4 показано, как использовать setWhatImReading () и доказывается, что переменная изменяется правильно:

Листинг 4. Тест-кейс атомарного апдейтера.

package com.geeckap.atomicexample;
 
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
 
public class AtomicExampleTest
{
    private MyObject obj;
 
    @Before
    public void setUp()
    {
        obj = new MyObject();
        obj.setWhatImReading( new Book( «Java 2 From Scratch» ) );
    }
 
    @Test
    public void testUpdate()
    {
        obj.setWhatImReading( new Book( 
                «Pro Java EE 5 Performance Management and Optimization» ) );
        Assert.assertEquals( «Incorrect book name», 
                «Pro Java EE 5 Performance Management and Optimization», 
                obj.getWhatImReading().getName() );
    }
 
}
 
В заключение.

Многопоточное программирование – это всегда испытание, но с тех пор, как платформа Java эволюционировала, она приобрела поддержку, которая упрощает некоторые многопоточные задачи программирования. В этой статье я рассмотрел пять вещей, которые вы могли не знать о написании многопоточных приложений на платформе Java, в том числе разницу между синхронизированными методами и блоками кода, значение использования ThreadLocal переменных, широкое недопонимание volatile (в том числе опасности полагаться на volatile, когда надо использовать синхронизацию), и краткий обзор тонкостей атомарных классов. Кто хочет знать больше, смотрите раздел Ссылки(на сайте автора).

volatile vs. volatile / Блог компании OTUS. Онлайн-образование / Хабр

Всем привет! Мы подготовили перевод данной статьи в преддверии старта курса «Разработчик C++»




Повесть о двух, казалось бы, похожих, но все же разных инструментах

Херб — автор бестселлеров и консультант по вопросам разработки программного обеспечения, а также архитектор ПО в Microsoft. Вы можете связаться с ним на www.gotw.ca.



Что означает ключевое слово volatile? Как его следует использовать? К всеобщему замешательству, существует два распространенных ответа, потому что в зависимости от языка, на котором вы пишете код, volatile относится к одной из двух различных техник программирования: lock-free программированию (без блокировок) и работе со «необычной» памятью. (См. Рисунок 1.)


Рисунок 1: повесть о двух технических требованиях.

Усугубляет путаницу и то, что эти два различных случая использования имеют частично совпадающие предпосылки и накладываемые ограничения, что заставляет их выглядеть более схожими, нежели они являются на самом деле. Давайте же четко определим и поймем их, и разберемся, как их правильно употреблять в C, C++, Java и C# — и всегда ли именно как volatile.


Таблица 1: Сравнение накладывающихся, но разных предпосылок.

Случай 1: Упорядоченные атомарные переменные для lock-free программирования


Lock-free программирование связано с налаживанием коммуникации и синхронизации между потоками с помощью инструментов более низкого уровня, нежели взаимоисключающие блокировки. Как в прошлом, так и сегодня существует широкий спектр таких инструментов. В грубом историческом порядке они включают явные барьеры (explicit fences/barriers — например, mb() в Linux), специальные упорядочивающие вызовы API (например, InterlockedExchange в Windows) и различные разновидности специальных атомарных типов. Многие из этих инструментов муторны и/или сложны, и их широкое разнообразие означает, что в конечном итоге lock-free код пишется в разных средах по-разному.

Однако в последние несколько лет наблюдается значительная конвергенция между поставщиками аппаратного и программного обеспечения: вычислительная индустрия объединяется вокруг последовательно согласованных упорядоченных атомарных переменных (ordered atomic variables) в качестве стандарта или единственного способа написания lock-free кода с использованием основных языков и платформ ОС. В двух словах, упорядоченные атомарные переменные безопасны для чтения и записи в нескольких потоках одновременно без каких-либо явных блокировок, поскольку они обеспечивают две гарантии: их чтение и запись гарантированно будут выполняться в том порядке, в котором они появляются в исходном коде вашей программы; и каждое чтение или запись гарантированно будут атомарными, “все или ничего”. У них также есть специальные операции, такие как compareAndSet, которые гарантированно выполняются атомарно. См. [1] для получения дополнительной информации об упорядоченных атомарных переменных и о том, как их правильно использовать.

Упорядоченные атомарные переменные доступны в Java, C# и других языках .NET, а также в готовящемся стандарте ISO C++, но под другими именами:

  • Java предоставляет упорядоченные атомарные переменные под ключевым словом volatile (например, volatile int), полностью поддерживая это с Java 5 (2004). Java дополнительно предоставляет несколько именованных типов в java.util.concurrent.atomic, например, AtomicLongArray, который вы можете использовать для тех же целей.
  • .NET добавил их в Visual Studio 2005, также под ключевым словом volatile (например, volatile int). Они подходят почти для любого варианта использования lock-free кода, за исключением редких примеров, подобных алгоритму Деккера. .NET исправляет оставшиеся ошибки в Visual Studio 2010, которая находится на стадии бета-тестирования на момент написания этой статьи.
  • ISO C++ добавил их в черновик стандарта C++ 0x в 2007 году под шаблонным именем atomic <T> (например, atomic). С 2008 года они стали доступны в Boost и некоторых других реализациях. [2]. Библиотека atomic ISO C++ также предоставляет C-совместимый способ написания этих типов и их операций (например, atomic_int), и они, вероятно, будут приняты ISO C в ближайшем будущем.

Пару слов об оптимизации


Мы рассмотрим, как упорядоченные атомарные переменные ограничивают оптимизацию, которую могут выполнять компиляторы, процессоры, эффекты кэширования и другие элементы вашей среды выполнения. Итак, давайте сначала кратко рассмотрим некоторые основные правила оптимизации.

Фундаментальное правило оптимизации практически во всех языках таково: оптимизации, которые переупорядочивают («трансформируют») выполнение вашего кода, всегда являются легитимными, только если они не меняют смысла программы, так что программа не может определить разницу между выполнением исходного кода и преобразованного. В некоторых языках это также известно как правило «as-if», которое получает свое название из-за того факта, что преобразованный код имеет те же наблюдаемые эффекты, «как если бы» (as if) исходный исходный код был выполнен в том виде, в котором он был изначально написан.

Это правило имеет двоякий эффект: во-первых, оптимизация никогда не должна позволять получить результат, который раньше был невозможен, или нарушать любые гарантии, на которые исходному коду было разрешено полагаться, включая семантику языка. Если мы дадим невозможный результат, в конце концов, программа и пользователь, безусловно, смогут заметить разницу, и это уже не «как если бы» мы выполнили исходный не преобразованный код.

Во-вторых, оптимизации позволено сократить набор возможных исполнений. Например, оптимизация может привести к тому, что некоторые потенциальные (но не гарантированные) чередования (изменение порядка выполнения инструкций — интерливинг) никогда не произойдут. Это нормально, потому что программа все равно не может рассчитывать на то, что они произойдут.

Упорядоченные атомарные переменные и оптимизация


Использование упорядоченных атомарных переменных ограничивает виды оптимизации, которые может выполнять ваш компилятор, процессор и система кэширования. [3] Стоит отметить два вида оптимизаций:
  • Оптимизации упорядоченных атомарных операций чтения и записи.
  • Оптимизации соседних обычных операций чтения и записи.

Во-первых, все упорядоченные атомарные операции чтения и записи в заданном потоке должны выполняться строго в порядке исходного кода, потому что это одна из фундаментальных гарантий упорядоченных атомарных переменных. Тем не менее, мы все еще можем выполнить некоторые оптимизации, в частности, оптимизации, которые имеют такой же эффект, как если бы этот поток просто всегда выполнялся так быстро, что в некоторых точках не возникало бы чередования с другим потоком.

Например, рассмотрим этот код, где a — упорядоченная атомарная переменная:

a = 1;  	// A
a = 2;  	// B

Допустимо ли для компилятора, процессора, кэша или другой части среды выполнения преобразовывать приведенный выше код в следующий, исключая избыточную запись в строке A?
// A ': OK: полностью исключить строку A 
a = 2;  	// B

Ответ: «Да». Это легитимно, потому что программа не может определить разницу; это “как если бы” этот поток всегда работал так быстро, что никакой другой поток, работающий параллельно, в принципе не может чередоваться между строками A и B, чтобы увидеть промежуточное значение. [4]

Аналогично, если a — упорядоченная атомарная переменная, а local — неразделяемая локальная переменная, допустимо преобразовать

a = 1;  	// C: запись в a
local = a;  // D: чтение из a

в
a = 1;  	// C: запись в a
local = 1;  // D': OK, применить "подстановку константы"

что исключает чтение из a. Даже если другой поток одновременно пытается выполнить запись в a, это “как если бы” этот поток всегда работал так быстро, что другому потоку никогда не удавалось чередовать строки C и D, чтобы изменить значение, прежде чем мы успеем записать наше собственное обратно в local.

Во-вторых, близлежащие обычные операции чтения и записи все еще могут быть переупорядочены вокруг упорядоченных атомарных, но с некоторыми ограничениями. В частности, как описано в [3], обычные операции чтения и записи не могут перемещаться вверх по отношению к упорядоченному атомарному чтению (от “после” к “до”) и не могут перемещаться вниз по отношению к упорядоченной атомарной записи (от “до” к “после”). Короче говоря, это может вывести их из критического раздела кода, и вы сможете писать программы, которые выиграют от этого в производительности. Для получения более подробной информации см. [3].

На этом все касательно lock-free программирования и упорядоченных атомарных переменных. А как насчет другого случая, в котором рассматриваются какие-то «волатильные» адреса?

Случай 2: Свободные от семантики переменные для памяти с «необычной» семантикой


  • Вторая необходимость — работать с «необычной» памятью, которая выходит за рамки модели памяти данного языка, где компилятор должен предполагать, что переменная может изменить значение в любое время и/или что чтение и запись могут иметь непознаваемую семантику и следствия. Классические примеры:
  • Аппаратные регистры, часть 1: Асинхронные изменения. Например, рассмотрим ячейку памяти М на пользовательской плате, которая подключена к прибору, который производит запись непосредственно в M. В отличие от обычной памяти, которая изменяется только самой программой, значение, хранящееся в M, может измениться в любое время, даже если ни один программный поток не пишет в нее; следовательно, компилятор не может делать никаких предположений о том, что значение будет стабильным.
  • Аппаратные регистры, часть 2: Семантика. Например, рассмотрим область памяти M на пользовательской плате, где запись в эту позицию всегда автоматически увеличивается на единицу. В отличие от обычного места в RAM памяти, компилятор даже не может предположить, что выполнение записи в M и последующее сразу после нее чтение из M обязательно прочитает то же значение, которое было записано.
  • Память, имеющая более одного адреса. Если данная ячейка памяти доступна с использованием двух разных адресов А1 и А2, компилятор или процессор может не знать, что запись в ячейку А1 может изменить значение в ячейке А2. Любая оптимизация, предполагающая? что запись в A1, не изменяет значение A2, будет ломать программу, и должна быть предотвращена.

Переменные в таких местах памяти являются неоптимизируемыми переменными, потому что компилятор не может безопасно делать какие-либо предположения о них вообще. Иными словами, компилятору нужно сказать, что такая переменная не участвует в обычной системе типов, даже если она имеет конкретный тип. Например, если ячейка памяти M или A1/A2 в вышеупомянутых примерах в программе объявлена как «int», то что это в действительности означает? Самое большее, что это может означать, это то, что она имеет размер и расположение int, но это не может означать, что он ведет себя как int — в конце концов, int не автоинкрементируют себя, когда вы записываете в него, или таинственным образом не изменяет свое значение, когда вы совершите запись во что-то похожее на другую переменную по другому адресу.

Нам нужен способ отключить все оптимизации для их чтения и записи. ISO C и C++ имеют портативный, стандартный способ сообщить компилятору, что это такая специальная переменная, которую он не должен оптимизировать: volatile.

Java и .NET не имеют сопоставимой концепции. В конце концов, управляемые среды должны знать полную семантику программы, которую они выполняют, поэтому неудивительно, что они не поддерживают память с «непознаваемой» семантикой. Но и Java, и .NET предоставляют аварийные шлюзы для выхода из управляемой среды и вызова нативного кода: Java предоставляет Java Native Interface (JNI), а .NET предоставляет Platform Invoke (P/Invoke). Однако в спецификации JNI [5] о volatile ничего не говорится и вообще не упоминается ни Java volatile, ни C/C++ volatile; аналогично, в документации P/Invoke не упоминается взаимодействие с .NET volatile или C/C++ volatile. Таким образом, для правильного доступа к неоптимизируемой области памяти в Java или .NET вы должны написать функции C/C++, которые используют C/C++ volatile для выполнения необходимой работы от имени вызывающего их уравляющего кода, чтобы они полностью инкапсулировали и скрывали volatile память (т. е. не принимали и не возвращали ничего volatile) и вызывать эти функции через JNI и P/Invoke.

Неоптимизируемые переменные и (не) оптимизация


Все операции чтения и записи неоптимизируемых переменных в заданном потоке должны выполняться точно так, как написаны; никакие оптимизации не допускаются вообще, потому что компилятор не может знать полную семантику переменной и когда и как ее значение может измениться. Это более строгие выражения (по сравнению с упорядоченными атомарными переменными), которую нужно выполнять только в порядке исходного кода.

Рассмотрим снова два преобразования, которые мы рассматривали ранее, но на этот раз заменим упорядоченную атомарную переменную a на неоптимизируемую (C/C++ volatile) переменную v:

v = 1;  // A
v = 2;  // B

Легитимно ли это преобразовать следующим образом, чтобы удалить явно лишнюю запись в строке A?
// A ': невалидно, нельзя исключить запись
v = 2;  // B

Ответ — нет, потому что компилятор не может знать, что исключение записи строки A в v не изменит смысла программы. Например, v может быть местоположением, к которому обращается пользовательское оборудование, которое ожидает увидеть значение 1 перед значением 2 и иначе не будет работать правильно.

Аналогично, если v неоптимизируемая переменная, а local — неразделяемая локальная переменная, преобразование недопустимо

v = 1;            // C: запись в v
local = v;        // C: чтение из v

в
a = 1;         	// C: запись в v
local = l;   // D': невалидно, нельзя совершить
// "подстановку константы"

для упразднения чтение из v. Например, v может быть аппаратным адресом, который автоматически увеличивается каждый раз при записи, так что запись 1 даст значение 2 при следующем считывании.

Во-вторых, что насчет соседних обычных операций чтения и записи — можно ли их переупорядочить вокруг неоптимизируемых? Сегодня нет практического портативного ответа, потому что реализации компилятора C/C++ сильно различаются и вряд ли в скором времени начнут движение к единообразию. Например, одна интерпретация Стандарта C++ гласит, что обычные операции чтения могут свободно перемещаться в любом направлении относительно чтения или записи volatile C/C++, а вот обычная запись вообще не может перемещаться относительно чтения или записи volatile C/C++ — что делает volatile C/C++ в то же время и менее и более ограничительным, чем упорядоченные атомарные операции. Некоторые поставщики компиляторов поддерживают эту интерпретацию; другие вообще не оптимизируют чтение или запись volatile; а третьи имеют свою собственную семантику.

Резюме


Для написания безопасного lock-free кода, который коммуницирует между потоками без использования блокировок, предпочитайте использовать упорядоченные атомарные переменные: Java/.NET volatile, C++0x atomic<T>и C-совместимый atomic_T.

Чтобы безопасно обмениваться данными со специальным оборудованием или другой памятью с необычной семантикой, используйте неоптимизируемые переменные: ISO C/C++ volatile. Помните, что чтение и запись этих переменных не обязательно должны быть атомарными.

И наконец, чтобы объявить переменную, которая имеет необычную семантику и обладает какой-либо из или же сразу всеми гарантиями атомарности и/или упорядочения, необходимыми для написания lock-free кода, только черновик стандарта ISO C++0x предоставляет прямой способ ее реализации: volatile atomic <T>.

Примечания
  1. Г. Саттер. «Writing Lock-Free Code: A Corrected Queue» (DDJ, октябрь 2008 г.). Доступно online тут.
  2. [2] См. www.boost.org.
  3. [3] Г. Саттер. «Apply Critical Sections Consistently» (DDJ, ноябрь 2007 г.). Доступно в Интернете тут.
  4. [4] Существует распространенное возражение: «В исходном коде другой поток мог видеть промежуточное значение, но это невозможно в преобразованном коде. Разве это не изменение наблюдаемого поведения?» ответ: «Нет», потому что программе никогда не гарантировалось, что она будет фактически чередоваться как раз вовремя, чтобы увидеть это значение; для этого потока уже был легитимный результат — он всегда работал так быстро, что чередование никогда не случалось. Опять же, то, что следует из этой оптимизации, так это уменьшает набор возможных исполнений, что всегда является легитимным.
  5. [5] С. Лянг. Java Native Interface: Руководство программиста и спецификация. (Прентис Холл, 1999). Доступно online тут.



Бесплатный вебинар: «Hello, World!» на фарси или как использовать Unicode в C++»

Java Dev Notes: Ключевое слово volatile

Ключевое слово volatile применяется к полям класса и в общем означает, что данное поле будет использоваться (модифицироваться) несколькими потоками.

Применение этого модификатора к полю класса означает:

  • Значение этого поля никогда не будет кэшироваться потоками в свою локальную область памяти. Все чтения и записи будут идти напрямую в «главную» память, т.е. ту, в которой и размещено поле класса
  • Доступ к переменной действует как будто бы она была заключена в блок synchronized, который синхронизируется по самому полю

Поясним первый пункт. Java позволяет потокам, которые имеют доступ к зашаренным переменным сохранять локальные копии этих переменных в своей области памяти для более эффективной работы. Точнее, это позволяет сделать саму реализацию многопоточности более эффективной. Эти «рабочии копии» должны быть синхронизированы с мастер-копией только лишь в заранее определенных точках, которые обусловлены синхронизацией — а именно, когда объект лочится (locks) и разлочивается (unlocks). Как правило, чтобы убедиться, что разделяемая переменная целостно и надежно (reliably) обновляется, поток должен быть уверенным, что он имеет эксклюзивный доступ к такой переменной. Это делается получением лока на переменную, который и обеспечивает взаимное исключение для доступа к этой переменной.

Для пояснения второго пункта приведем табличку для сравнения volatile и synchronized. Не зря во втором пункте использовано слово «как будто»: Доступ к переменной действует как будто бы она была заключена в блок synchronized. Т.к. явно в коде монитор, по которому синхронизируется доступ к переменной, не используется….

Итак, табличка:

Свойствоsynchronizedvolatile
Тип переменнойТолько объектОбъект или примитив
Разрешен ли nullнетда
Может ли быть блокировкаданет
Когда происходит синхронизацияКогда явно происходит вход в блок synchronizedКаждый раз при доступе к переменной

Дополнительно см: The volatile keyword in Java

Volatile Vs Static в java

проще говоря,

  1. статический : static переменные связаны с класс, а не с какой-либо объект. Каждый экземпляр класса разделяет переменную класса, которая находится в одном фиксированном месте в памяти

  2. летучие: это ключевое слово применимо как класс и экземпляр переменные.

использование изменчивых переменных снижает риск ошибок согласованности памяти, поскольку любая запись в изменчивую переменную устанавливает связь «происходит до» с последующими считываниями той же переменной. Это означает, что изменения изменчивой переменной всегда видны другим потокам

посмотри статьи by Javin Paul чтобы понять изменчивые переменные в лучшем случае.

в отсутствие volatile ключевое слово, значение переменной в стеке каждого потока может быть разной. Сделав переменную как volatile, все потоки получат одинаковое значение в своей рабочей памяти и ошибок согласованности памяти было избежать.

здесь variable может быть static (класса) переменных или instance (объект) переменной.

относительно вашего запроса :

в любом случае значение статической переменной также будет одним значением для всех потоков, тогда почему мы должны идти на volatile?

если мне понадобится instance переменной в моем приложении, я не могу использовать static переменной. Даже в случае static переменная, согласованность не гарантируется из-за кэша потоков, как показано на диаграмме.

используя volatile переменные снижает риск ошибок согласованности памяти, потому что любая запись в volatile-переменную устанавливает отношение happens-before с последующими считываниями той же переменной. Это означает, что изменения изменчивой переменной всегда видны другим потокам.

более того, это также означает, что когда поток читает переменную volatile, он видит не только последнее изменение volatile, но и побочные эффекты кода, который привел к изменению => ошибки согласованности памяти по-прежнему возможны с изменчивыми переменными. Чтобы избежать побочных эффектов, необходимо использовать синхронизированные переменные. Но в java есть лучшее решение.

использование простого атомарного доступа к переменным более эффективно, чем доступ к этим переменным через синхронизированный код

некоторые классы java.util.concurrent пакет предоставляет атомарные методы, которые не зависят от синхронизации.

относятся к этому управление параллелизмом высокого уровня статьи новые подробности.

особенно посмотрите на атомарные переменные.

связанные вопросы SE:

Volatile Vs Atomic

летучие булевых против AtomicBoolean

разница между volatile и synchronized в Java

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Theme: Overlay by Kaira Extra Text
Cape Town, South Africa