Вопрос-ответ

Difference between volatile and synchronized in Java

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

Мне интересно, в чем разница между объявлением переменной как volatile и постоянным доступом к переменной в synchronized(this) блоке в Java?

Согласно этой статье http://www.javamex.com/tutorials/synchronization_volatile.shtml можно многое сказать, и есть много различий, но также и некоторые сходства.

Меня особенно интересует эта информация:


...



  • доступ к volatile переменной никогда не может быть заблокирован: мы всегда выполняем только простое чтение или запись, поэтому, в отличие от синхронизированного блока, мы никогда не будем удерживать какую-либо блокировку;

  • поскольку доступ к volatile переменной никогда не блокируется, он не подходит для случаев, когда мы хотим читать-обновлять-записывать как атомарную операцию (если только мы не готовы "пропустить обновление");


Что они подразумевают под чтением-обновлением-записью? Разве запись не является также обновлением или они просто означают, что обновление - это запись, которая зависит от чтения?

Прежде всего, когда больше подходит объявлять переменные, volatile а не обращаться к ним через synchronized блок? Хорошая ли идея использовать volatile для переменных, которые зависят от входных данных? Например, существует переменная с именем render, которая считывается через цикл рендеринга и устанавливается событием нажатия клавиши?

Переведено автоматически
Ответ 1

Важно понимать, что у потокобезопасности есть два аспекта.


  1. управление выполнением и

  2. видимость памяти

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

Использование synchronized предотвращает получение любым другим потоком монитора (или блокировки) для того же объекта, тем самым предотвращая одновременное выполнение всех блоков кода, защищенных синхронизацией, для одного и того же объекта. Синхронизация также создает барьер памяти "происходит до", вызывая ограничение видимости памяти, так что все, что делается до того момента, когда какой-либо поток снимает блокировку, кажется другому потоку, который впоследствии получает ту же блокировку, что произошло до того, как он получил блокировку. С практической точки зрения, на текущем оборудовании это обычно приводит к очистке кэшей процессора при получении монитора и записи в основную память при ее освобождении, оба из которых являются (относительно) дорогостоящими.

Использование volatile, с другой стороны, вынуждает все обращения (чтение или запись) к volatile переменной выполняться в основной памяти, эффективно удаляя volatile переменную из кэшей процессора. Это может быть полезно для некоторых действий, где просто требуется, чтобы видимость переменной была правильной, а порядок обращений не важен. Использование volatile также изменяет обработку long и double, требуя, чтобы обращения к ним были атомарными; на некоторых (более старых) аппаратных средствах для этого могут потребоваться блокировки, но не на современном 64-разрядном оборудовании. В новой модели памяти (JSR-133) для Java 5+ семантика volatile была усилена, чтобы быть почти такой же сильной, как synchronized, в отношении видимости памяти и порядка команд (см. http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile). Для наглядности каждый доступ к полю volatile выполняется как половина синхронизации.


В соответствии с новой моделью памяти по-прежнему верно, что volatile переменные не могут быть переупорядочены друг с другом. Разница в том, что теперь уже не так просто изменить порядок обычных обращений к полям вокруг них. Запись в volatile поле имеет тот же эффект памяти, что и освобождение монитора, а чтение из volatile поля имеет тот же эффект памяти, что и получение монитором. По сути, поскольку новая модель памяти накладывает более строгие ограничения на переупорядочивание обращений к volatile полю с помощью других обращений к полю, volatile или нет, все, что было видно потоку A при записи в volatile field f, становится видимым потоку B при чтении f.


-- Часто задаваемые вопросы по JSR 133 (Java Memory Model)


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

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

// Declaration
public class SharedLocation {
static public volatile SomeObject someObject=new SomeObject(); // default object
}

// Publishing code
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.

Отвечая, в частности, на ваш вопрос о чтении-обновлении-записи. Рассмотрите следующий небезопасный код:

public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}

Теперь, когда метод updateCounter() не синхронизирован, два потока могут вводить его одновременно. Среди множества возможных вариантов того, что может произойти, один заключается в том, что поток-1 выполняет тест для counter == 1000 и находит его истинным, а затем приостанавливается. Затем поток-2 выполняет тот же тест, также видит, что он верен, и приостанавливается. Затем поток-1 возобновляется и устанавливает счетчик в 0. Затем поток-2 возобновляется и снова устанавливает счетчик в 0, потому что он пропустил обновление от потока-1. Это также может произойти, даже если переключение потоков происходит не так, как я описал, а просто потому, что две разные кэшированные копии counter присутствовали в двух разных ядрах процессора, и каждый поток выполнялся на отдельном ядре. Если уж на то пошло, один поток может иметь счетчик с одним значением, а другой может иметь счетчик с каким-то совершенно другим значением просто из-за кэширования.

Что важно в этом примере, так это то, что переменная counter считывалась из основной памяти в кэш, обновлялась в кэше и записывалась обратно в основную память только в какой-то неопределенный момент позже, когда возникал барьер памяти или когда кэш-память требовалась для чего-то другого. Создание счетчика volatile недостаточно для потокобезопасности этого кода, потому что проверка на максимум и присвоения являются дискретными операциями, включая приращение, которое представляет собой набор неатомных read+increment+write машинных инструкций, что-то вроде:

MOV EAX,counter
INC EAX
MOV counter,EAX

Изменяемые переменные полезны только тогда, когда все операции, выполняемые над ними, являются "атомарными", как в моем примере, когда ссылка на полностью сформированный объект только считывается или записывается (и, действительно, обычно она записывается только из одной точки). Другим примером может быть ссылка на volatile массив, поддерживающая список копирования при записи, при условии, что массив был прочитан только путем предварительного создания локальной копии ссылки на него.

Ответ 2

volatile - это модификатор поля, в то время как synchronized изменяет блоки кода и методы. Итак, мы можем указать три варианта простого средства доступа, используя эти два ключевых слова:


    int i1;
int geti1() {return i1;}

volatile int i2;
int geti2() {return i2;}

int i3;
synchronized int geti3() {return i3;}

geti1() доступ к значению, хранящемуся в данный момент в i1 текущем потоке.
Потоки могут иметь локальные копии переменных, и данные не обязательно должны совпадать с данными, хранящимися в других потоках.В частности, другой поток мог обновить i1 в своем потоке, но значение в текущем потоке может отличаться от этого обновленного значения. На самом деле в Java есть идея "основной" памяти, и это память, которая хранит текущее "правильное" значение для переменных. Потоки могут иметь свою собственную копию данных для переменных, и копия потока может отличаться от "основной" памяти. Таким образом, фактически, "основная" память может иметь значение 1 для i1, для thread1 иметь значение 2 для i1 и для thread2 иметь значение 3 для i1, если thread1 и thread2 обновили i1, но эти обновленные значения еще не были переданы в "основную" память или другие потоки.


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


Между volatile и synchronized есть два различия.


Во-первых, synchronized получает и снимает блокировки на мониторах, которые могут заставить только один поток одновременно выполнять блок кода. Это довольно хорошо известный аспект synchronized . Но synchronized также синхронизирует память. Фактически synchronized синхронизирует всю память потока с "основной" памятью. Итак, выполнение geti3() выполняет следующее:



  1. Поток получает блокировку на мониторе для объекта this .

  2. Память потока очищает все свои переменные, т. Е. Все его переменные эффективно считываются из "основной" памяти .

  3. Выполняется блок кода (в данном случае устанавливается возвращаемое значение равным текущему значению i3, которое, возможно, только что было сброшено из "основной" памяти).

  4. (Обычно любые изменения переменных теперь записываются в "основную" память, но для geti3() у нас нет изменений.)

  5. Поток снимает блокировку на мониторе для объекта this.


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


http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html

Ответ 3

Есть 3 основные проблемы с многопоточностью:


  1. Условия гонки



  2. Кэширование / устаревшая память



  3. Оптимизация компилятора и процессора



volatile может решить 2 и 3, но не может решить 1. synchronized/ явные блокировки могут решить 1, 2 и 3.

Разработка:


  1. Считайте этот поток небезопасным кодом:

x++;

Хотя это может выглядеть как одна операция, на самом деле их 3: считывание текущего значения x из памяти, добавление к нему 1 и сохранение его обратно в память. Если несколько потоков пытаются выполнить это одновременно, результат операции не определен. Если x изначально было 1, то после 2 потоков, работающих с кодом, может быть 2, а может быть и 3, в зависимости от того, какой поток выполнил какую часть операции до того, как управление было передано другому потоку. Это форма условия гонки.

Использование synchronized для блока кода делает его атомарным, то есть создается впечатление, что 3 операции выполняются одновременно, и другой поток не может вмешаться в середину. Итак, если x было 1, и 2 потока пытаются выполнить предварительную обработку, x++ мы знаем, что в итоге это будет равно 3. Таким образом, это решает проблему состояния гонки.

synchronized (this) {
x++; // no problem now
}

Пометка x как volatile не делает x++; атомарной, поэтому проблему это не решает.


  1. Кроме того, потоки имеют свой собственный контекст, т.Е. Они могут кэшировать значения из основной памяти. Это означает, что несколько потоков могут иметь копии переменной, но они работают со своей рабочей копией, не передавая новое состояние переменной другим потокам.

Рассмотрим это в одном потоке, x = 10;. И несколько позже, в другом потоке, x = 20;. Изменение значения x может не отображаться в первом потоке, потому что другой поток сохранил новое значение в своей рабочей памяти, но не скопировал его в основную память. Или в том, что он скопировал его в основную память, но первый поток не обновил свою рабочую копию. Итак, если теперь первый поток проверяет, if (x == 20) ответ будетfalse.

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

Обратите внимание, что в отличие от обработки данных, устаревшую память не так просто создать (повторно), поскольку сброс в основную память происходит в любом случае.


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

Рассмотрим следующий код:

boolean b = false;
int x = 10;

void threadA() {
x = 20;
b = true;
}

void threadB() {
if (b) {
System.out.println(x);
}
}

Можно подумать, что ThreadB может напечатать только 20 (или вообще ничего не печатать, если ThreadB if-check выполняется перед установкой b значения true), поскольку b значение true устанавливается только после того, как x установлено значение 20, но компилятор / центральный процессор может решить изменить порядок потоков, в этом случае ThreadB также может напечатать 10. Пометка b как volatile гарантирует, что она не будет переупорядочена (или отброшена в определенных случаях). Что означает, что ThreadB может напечатать только 20 (или вообще ничего). Пометка методов как синхронизированных приведет к тому же результату. Также пометка переменной как volatile только гарантирует, что она не будет переупорядочена, но все, что до / после нее, все еще может быть переупорядочено, поэтому синхронизация может быть более подходящей в некоторых сценариях.

Обратите внимание, что до появления новой модели памяти Java 5 volatile не решал эту проблему.

Ответ 4

синхронизировано

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



  1. Два выполнения synchronized методов на одном объекте никогда не выполняются

  2. Изменение состояния объекта видно другим потокам


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

Хороший пример использования volatile variable : Date переменная.

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

Лоуренс Дол Клири объяснил вам read-write-update query.

Что касается других ваших запросов

Когда больше подходит объявлять переменные volatile, чем обращаться к ним через synchronized?

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

Хорошая ли идея использовать volatile для переменных, которые зависят от входных данных?

Ответ будет таким же, как и в первом запросе.

java multithreading