Разница между изменчивым и синхронизированным в Java
Меня интересует разница между объявлением переменной как volatile
и всегда обращаясь к переменной в synchronized(this)
блок в Java?
В соответствии с этой статьей http://www.javamex.com/tutorials/synchronization_volatile.shtml многое можно сказать и есть много различий, но также есть и некоторые сходства.
Я особенно заинтересован в этой части информации:
...
- доступ к энергозависимой переменной никогда не может блокировать: мы только делаем простое чтение или запись, поэтому в отличие от синхронизированного блока, мы никогда не будем удерживать блокировку;
- поскольку доступ к энергозависимой переменной никогда не удерживает блокировку, он не подходит для случаев, когда мы хотим читать-обновлять-записывать как элементарную операцию (если мы не готовы "пропустить обновление");
Что они подразумевают под чтением-обновлением-записью? Разве запись не является обновлением или они просто означают, что обновление - это запись, которая зависит от чтения?
Больше всего, когда лучше объявить переменные volatile
а не доступ к ним через synchronized
блок? Это хорошая идея для использования volatile
для переменных, которые зависят от ввода? Например, есть переменная с именем render
что читается через цикл рендеринга и устанавливается с помощью события нажатия клавиши?
5 ответов
Важно понимать, что есть два аспекта безопасности потоков.
- контроль исполнения и
- видимость памяти
Первый связан с управлением, когда код выполняется (включая порядок выполнения инструкций) и может ли он выполняться одновременно, а второй - с тем, когда эффекты в памяти того, что было сделано, видны другим потокам. Поскольку каждый ЦП имеет несколько уровней кеша между ним и основной памятью, потоки, работающие на разных ЦП или ядрах, могут видеть "память" по-разному в любой момент времени, поскольку потокам разрешено получать и работать с частными копиями основной памяти.
С помощью synchronized
не позволяет любому другому потоку получить монитор (или блокировку) для того же объекта, тем самым предотвращая одновременное выполнение всех блоков кода, защищенных синхронизацией на одном и том же объекте. Синхронизация также создает барьер памяти "происходит раньше", вызывая ограничение видимости памяти, так что все, что было сделано до того момента, когда какой-то поток освобождает блокировку, отображается в другом потоке, впоследствии получающем такую же блокировку, которая произошла до того, как он получил блокировку. С практической точки зрения, на современном оборудовании это обычно вызывает сброс кэшей ЦП при получении монитора и запись в основную память при его освобождении, оба из которых (относительно) дороги.
С помощью volatile
с другой стороны, принудительно все обращения (чтение или запись) к переменной volatile происходят в основную память, что эффективно удерживает переменную volatile из кэшей ЦП. Это может быть полезно для некоторых действий, когда просто требуется, чтобы видимость переменной была правильной, а порядок обращений не важен. С помощью volatile
также меняет лечение long
а также double
требовать, чтобы доступ к ним был атомарным; на некоторых (более старых) устройствах это может потребовать блокировок, но не на современном 64-разрядном оборудовании. В новой (JSR-133) модели памяти для Java 5+ семантика volatile была усилена и стала почти такой же сильной, как и синхронизированная, в отношении видимости памяти и порядка команд (см. http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html). В целях наглядности каждый доступ к изменчивому полю действует как половина синхронизации.
Согласно новой модели памяти, все еще верно, что изменчивые переменные не могут быть переупорядочены друг с другом. Разница в том, что теперь уже не так легко переупорядочить обычные полевые доступы вокруг них. Запись в энергозависимое поле имеет тот же эффект памяти, что и отпускание монитора, а чтение из энергозависимого поля имеет тот же эффект памяти, что и захват монитора. Фактически, поскольку новая модель памяти накладывает более строгие ограничения на изменение порядка доступа к изменяемым полям с использованием других обращений к полям, изменяемых или нет, всего, что было видно потоку
A
когда он пишет в изменчивое полеf
становится видимым для потокаB
когда он читаетf
,
Итак, теперь обе формы барьера памяти (в рамках текущего JMM) вызывают барьер переупорядочения команд, который не позволяет компилятору или среде выполнения переупорядочивать инструкции через барьер. В старом JMM, volatile не помешал переупорядочению. Это может быть важно, поскольку помимо барьеров памяти наложено единственное ограничение, заключающееся в том, что для любого конкретного потока суммарный эффект кода такой же, каким он был бы, если бы инструкции выполнялись именно в том порядке, в котором они появляются в источник.
Одно из применений volatile предназначено для общего, но неизменного объекта, воссоздаемого на лету, причем многие другие потоки принимают ссылку на объект в определенной точке их цикла выполнения. Нужно, чтобы другие потоки начали использовать воссозданный объект после его публикации, но не требуют дополнительных накладных расходов на полную синхронизацию и сопутствующие конфликты и очистку кеша.
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing 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.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
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() не синхронизирован, два потока могут войти в него одновременно. Среди множества вариантов того, что может произойти, одна из них заключается в том, что thread-1 выполняет тест для counter==1000, находит его верным и затем приостанавливается. Затем thread-2 выполняет тот же тест, а также видит его верным и приостанавливается. Затем поток-1 возобновляет работу и устанавливает счетчик на 0. Затем поток-2 возобновляет работу и снова устанавливает счетчик на 0, поскольку он пропустил обновление из потока-1. Это также может произойти, даже если переключение потоков происходит не так, как я описал, а просто потому, что две разные кэшированные копии счетчика присутствовали в двух разных ядрах ЦП, и каждый из потоков работал на отдельном ядре. В этом отношении один поток может иметь счетчик с одним значением, а другой - с каким-то совершенно другим значением только из-за кэширования.
В этом примере важно то, что переменный счетчик считывался из основной памяти в кеш, обновлялся в кеше и записывался обратно в основную память только в какой-то неопределенный момент позже, когда возник барьер памяти или когда кеш-память была нужна для чего-то еще. Делать счетчик volatile
недостаточно для обеспечения безопасности потока в этом коде, потому что проверка на максимум и присвоения являются дискретными операциями, включая приращение, которое представляет собой набор неатомарных read+increment+write
машинные инструкции, что-то вроде:
MOV EAX,counter
INC EAX
MOV counter,EAX
Изменчивые переменные полезны только тогда, когда все операции, выполняемые над ними, являются "атомарными", как, например, мой пример, когда ссылка на полностью сформированный объект только для чтения или записи (и, как правило, обычно она пишется только из одной точки). Другим примером может быть изменчивая ссылка на массив, поддерживающая список копирования при записи, при условии, что массив был прочитан только при первом получении локальной копии ссылки на него.
volatile является модификатором поля, а синхронизированный изменяет блоки кода и методы. Таким образом, мы можем указать три варианта простого средства доступа, используя эти два ключевых слова:
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, должна синхронизировать свои данные во всех потоках, чтобы при каждом обращении к переменной или ее изменении в любом потоке все остальные потоки сразу видели одно и то же значение. Обычно изменчивые переменные имеют более высокий уровень доступа и обновления, чем "обычные" переменные. Обычно потокам разрешено иметь собственную копию данных для большей эффективности.Есть два различия между volitile и синхронизированными.
Сначала синхронизируется получает и снимает блокировки на мониторах, которые могут заставить только один поток за один раз выполнить блок кода. Это довольно известный аспект синхронизации. Но синхронизируется и синхронизирует память. Фактически синхронизированный синхронизирует всю память потока с "основной" памятью. Так исполняет
geti3()
делает следующее:
- Поток получает блокировку на мониторе для объекта this.
- Память потока сбрасывает все свои переменные, то есть все переменные эффективно считываются из "основной" памяти.
- Кодовый блок выполняется (в этом случае установка возвращаемого значения на текущее значение i3, которое, возможно, только что было сброшено из "основной" памяти).
- (Любые изменения в переменных теперь обычно записываются в "основную" память, но для geti3() у нас нет изменений.)
- Поток снимает блокировку на мониторе для объекта this.
Поэтому, когда volatile синхронизирует только значение одной переменной между памятью потока и "основной" памятью, синхронизированный синхронизирует значение всех переменных между памятью потока и "основной" памятью, а также блокирует и освобождает монитор для загрузки. Четкая синхронизация, вероятно, будет иметь больше накладных расходов, чем энергозависимых.
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
тл; др:
Есть 3 основных проблемы с многопоточностью:
1) Условия гонки
2) Кеширование / устаревшая память
3) оптимизация Complier и CPU
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++;
атомарный, так что это не решает эту проблему.
2) Кроме того, потоки имеют собственный контекст - то есть они могут кэшировать значения из основной памяти. Это означает, что несколько потоков могут иметь копии переменной, но они работают со своей рабочей копией, не разделяя новое состояние переменной среди других потоков.
Считайте, что в одном потоке, x = 10;
, А чуть позже, в другой ветке, x = 20;
, Изменение стоимости x
может не отображаться в первом потоке, поскольку другой поток сохранил новое значение в своей рабочей памяти, но не скопировал его в основную память. Или что он скопировал его в основную память, но первый поток не обновил свою рабочую копию. Так что если сейчас первый поток проверяет if (x == 20)
ответ будет false
,
Маркировка переменной как volatile
в основном говорит всем потокам выполнять операции чтения и записи только в основной памяти. synchronized
приказывает каждому потоку обновлять свое значение из основной памяти при входе в блок и сбрасывать результат обратно в основную память при выходе из блока.
Обратите внимание, что в отличие от гонок данных, устаревшая память не так легко (пере) создать, так как в любом случае происходит сброс к основной памяти.
3) Complier и CPU могут (без какой-либо синхронизации между потоками) обрабатывать весь код как однопоточный. Это означает, что он может посмотреть на некоторый код, который очень важен в многопоточном аспекте, и рассматривать его как однопоточный, где он не так важен. Поэтому он может посмотреть на код и решить, ради оптимизации, изменить его порядок или даже полностью удалить его части, если он не знает, что этот код предназначен для работы в нескольких потоках.
Рассмотрим следующий код:
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
правда), а b
устанавливается в true только после x
установлен в 20, но компилятор / ЦП может решить изменить порядок потока A, в этом случае поток B также может вывести 10. Маркировка b
как volatile
гарантирует, что он не будет переупорядочен (или исключен в некоторых случаях). Что означает, что threadB может печатать только 20 (или вообще ничего). Маркировка методов как синхронизированных приведет к тому же результату. Также помечая переменную как volatile
только гарантирует, что он не будет переупорядочен, но все до / после него все еще может быть переупорядочено, поэтому синхронизация может быть более подходящей в некоторых сценариях.
Обратите внимание, что до появления Java 5 New Memory Model, volatile не решала эту проблему.
synchronized
модификатор ограничения доступа уровня уровня / блока. Это гарантирует, что один поток владеет блокировкой для критического раздела. Только нить, которой принадлежит блокировка, может войти synchronized
блок. Если другие потоки пытаются получить доступ к этому критическому разделу, они должны ждать, пока текущий владелец не снимет блокировку.
volatile
это модификатор доступа к переменной, который заставляет все потоки получать последнее значение переменной из основной памяти. Блокировка не требуется для доступа volatile
переменные. Все потоки могут получить доступ к значению переменной переменной одновременно.
Хороший пример использования переменной volatile: Date
переменная.
Предположим, что вы сделали переменную Date volatile
, Все потоки, которые обращаются к этой переменной, всегда получают последние данные из основной памяти, так что все потоки показывают реальное (фактическое) значение даты. Вам не нужны разные потоки, показывающие разное время для одной и той же переменной. Все темы должны показывать правильное значение даты.
Посмотрите на эту статью для лучшего понимания volatile
концепция.
Лоуренс Дол Клири объяснил ваш read-write-update query
,
Что касается других ваших запросов
Когда более целесообразно объявлять переменные изменчивыми, чем обращаться к ним через синхронизированные?
Вы должны использовать volatile
если вы думаете, что все потоки должны получить действительное значение переменной в реальном времени, как в примере, который я объяснил для переменной Date.
Это хорошая идея использовать volatile для переменных, которые зависят от ввода?
Ответ будет таким же, как в первом запросе.
Обратитесь к этой статье для лучшего понимания.
volatile
Ключевое слово в Java является модификатором поля, в то время какsynchronized
изменяет блоки кода и методы.synchronized
получает и снимает блокировку на java монитораvolatile
Ключевое слово не требует этого.Потоки в Java могут быть заблокированы для ожидания любого монитора в случае
synchronized
это не так сvolatile
Ключевое слово в Java.synchronized
метод влияет на производительность больше, чемvolatile
Ключевое слово в Java.поскольку
volatile
ключевое слово в Java только синхронизирует значение одной переменной между памятью потока и "основной" памятью, в то время какsynchronized
Ключевое слово синхронизирует значение всех переменных между памятью потока и "основной" памятью и блокирует и освобождает монитор для загрузки. По этой причинеsynchronized
Ключевое слово в Java, вероятно, будет иметь больше накладных расходов, чемvolatile
,Вы не можете синхронизировать на нулевом объекте, но ваш
volatile
переменная в Java может быть нулевой.Из Java 5 Запись в
volatile
поле имеет тот же эффект памяти, что и выпуск монитора, а чтение из энергозависимого поля имеет тот же эффект памяти, что и захват монитора
Мне нравится объяснение Дженкова
Видимость общих объектов
Если два или более потоков совместно используют объект, без надлежащего использования либо изменчивых объявлений, либо синхронизации, обновления общего объекта, сделанные одним потоком, могут быть невидимы для других потоков.
Представьте, что общий объект изначально хранится в основной памяти. Поток, работающий на ЦП 1, затем считывает общий объект в свой кэш ЦП. Там он вносит изменения в общий объект. Пока кэш ЦП не был сброшен обратно в основную память, измененная версия общего объекта не видна потокам, работающим на других ЦП. Таким образом, каждый поток может получить свою собственную копию общего объекта, каждая копия будет находиться в отдельном кеше ЦП.
Следующая диаграмма иллюстрирует набросок ситуации. Один поток, работающий на левом ЦП, копирует общий объект в свой кэш ЦП и изменяет свою переменную count на 2. Это изменение невидимо для других потоков, работающих на правом ЦП, поскольку обновление для счета не было сброшено обратно в основной памяти еще нет.
Чтобы решить эту проблему, вы можете использовать ключевое слово volatile для Java. Ключевое слово volatile может гарантировать, что данная переменная считывается непосредственно из основной памяти и всегда записывается обратно в основную память при обновлении.
Условия гонки
Если два или более потоков совместно используют объект и более одного потока обновляют переменные в этом общем объекте, могут возникнуть условия гонки.
Представьте, что поток A считывает переменную count общего объекта в свой кэш процессора. Представьте также, что поток B делает то же самое, но в другой кэш процессора. Теперь поток A добавляет один к счету, а поток B делает то же самое. Теперь var1 был увеличен в два раза, один раз в каждом кэше процессора.
Если бы эти приращения были выполнены последовательно, счетчик переменных был бы увеличен в два раза и имел исходное значение + 2, записанное обратно в основную память.
Тем не менее, два приращения были выполнены одновременно без надлежащей синхронизации. Независимо от того, какой из потоков A и B, который записывает свою обновленную версию count в основную память, обновленное значение будет только на 1 больше исходного значения, несмотря на два приращения.
Эта диаграмма иллюстрирует возникновение проблемы с условиями гонки, как описано выше:
Для решения этой проблемы вы можете использовать синхронизированный блок Java. Синхронизированный блок гарантирует, что только один поток может войти в данный критический раздел кода в любой момент времени. Синхронизированные блоки также гарантируют, что все переменные, к которым обращаются внутри синхронизированного блока, будут считаны из основной памяти, и когда поток выйдет из синхронизированного блока, все обновленные переменные будут снова сброшены в основную память, независимо от того, объявлена ли переменная volatile или не.