Важно понимать, что есть два аспекта безопасности потоков.
- контроль исполнения и
- видимость памяти
Первый связан с управлением, когда код выполняется (включая порядок выполнения инструкций) и может ли он выполняться одновременно, а второй - с тем, когда эффекты в памяти того, что было сделано, видны другим потокам. Поскольку каждый ЦП имеет несколько уровней кэша между ним и основной памятью, потоки, работающие на разных ЦП или ядрах, могут видеть «память» по-разному в любой момент времени, поскольку потокам разрешено получать и работать с частными копиями основной памяти.
Использование не 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#volatile). Для наглядности каждый доступ к изменчивому полю действует как половина синхронизации.
В новой модели памяти все еще верно, что изменчивые переменные не могут быть переупорядочены друг с другом. Разница в том, что теперь уже не так легко переупорядочить обычные полевые доступы вокруг них. Запись в энергозависимое поле имеет тот же эффект памяти, что и отпускание монитора, а чтение из энергозависимого поля имеет тот же эффект памяти, что и захват монитора. Фактически, поскольку новая модель памяти накладывает более строгие ограничения на изменение порядка доступа к изменяемым полям с другими доступами к полям, изменяемыми или нет, все, что было видно потоку A
при записи в изменяемое поле, f
становится видимым потоку B
при чтении f
.
- Часто задаваемые вопросы по JSR 133 (модель памяти Java)
Итак, теперь обе формы барьера памяти (в соответствии с текущим 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
Изменчивые переменные полезны только тогда, когда все операции, выполняемые над ними, являются «атомарными», как, например, в моем примере, когда ссылка на полностью сформированный объект только для чтения или записи (и, как правило, обычно она пишется только из одной точки). Другим примером может быть изменчивая ссылка на массив, поддерживающая список копирования при записи, при условии, что массив был прочитан только при первом получении локальной копии ссылки на него.
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
источник
synchronized
модификатор ограничения доступа уровня уровня / блока. Это гарантирует, что один поток владеет блокировкой для критического раздела. Только поток, которому принадлежит блокировка, может войти вsynchronized
блок. Если другие потоки пытаются получить доступ к этому критическому разделу, они должны ждать, пока текущий владелец не снимет блокировку.volatile
является модификатором доступа к переменной, который заставляет все потоки получать последнее значение переменной из основной памяти. Для доступа кvolatile
переменным блокировка не требуется . Все потоки могут получить доступ к значению переменной переменной одновременно.Хороший пример использования volatile variable:
Date
variable.Предположим, что вы сделали переменную Date
volatile
. Все потоки, которые обращаются к этой переменной, всегда получают последние данные из основной памяти, так что все потоки показывают реальное (фактическое) значение даты. Вам не нужны разные потоки, показывающие разное время для одной и той же переменной. Все темы должны показывать правильное значение даты.Посмотрите на эту статью для лучшего понимания
volatile
концепции.Лоуренс Дол Клири объяснил
read-write-update query
.Что касается других ваших запросов
Вы должны использовать,
volatile
если считаете, что все потоки должны получать действительное значение переменной в реальном времени, как в примере, который я объяснил для переменной Date.Ответ будет таким же, как в первом запросе.
Обратитесь к этой статье для лучшего понимания.
источник
тл; др :
Есть 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. Таким образом, это решает проблему состояния гонки.Маркировка
x
какvolatile
не делаетx++;
атомарным, так что не решает эту проблему.2) Кроме того, потоки имеют свой собственный контекст - то есть они могут кэшировать значения из основной памяти. Это означает, что несколько потоков могут иметь копии переменной, но они работают со своей рабочей копией, не разделяя новое состояние переменной среди других потоков.
Считайте, что в одной теме
x = 10;
. А несколько позже, в другой веткеx = 20;
. Изменение значенияx
может не отображаться в первом потоке, поскольку другой поток сохранил новое значение в своей рабочей памяти, но не скопировал его в основную память. Или что он скопировал его в основную память, но первый поток не обновил свою рабочую копию. Так что если сейчас первая ветка проверяет,if (x == 20)
ответ будетfalse
.Пометка переменной как, по
volatile
сути, указывает всем потокам выполнять операции чтения и записи только в основной памяти.synchronized
приказывает каждому потоку обновлять свое значение из основной памяти при входе в блок и сбрасывать результат обратно в основную память при выходе из блока.Обратите внимание, что в отличие от гонок данных, устаревшая память не так легко (пере) создать, так как в любом случае происходит сброс к основной памяти.
3) Complier и CPU могут (без какой-либо синхронизации между потоками) обрабатывать весь код как однопоточный. Это означает, что он может посмотреть на некоторый код, который очень важен в многопоточном аспекте, и рассматривать его как однопоточный, где он не так важен. Таким образом, он может посмотреть на код и решить, ради оптимизации, изменить его порядок или даже полностью удалить его части, если он не знает, что этот код предназначен для работы в нескольких потоках.
Рассмотрим следующий код:
Вы могли бы подумать, что threadB может печатать только 20 (или вообще ничего не печатать, если threadB if-check выполняется перед установкой
b
в true), так какb
устанавливается в true только после того,x
как установлено в 20, но компилятор / ЦП может решить изменить порядок threadA, в этом случае threadB также может вывести 10. Пометкаb
asvolatile
гарантирует, что он не будет переупорядочен (или в некоторых случаях отброшен). Что означает, что threadB может печатать только 20 (или вообще ничего). Маркировка методов как синхронизированных приведет к тому же результату. Также пометка переменной какvolatile
только гарантирует, что она не будет переупорядочена, но все до / после нее все еще может быть переупорядочено, поэтому синхронизация может быть более подходящей в некоторых сценариях.Обратите внимание, что до появления Java 5 New Memory Model, volatile не решала эту проблему.
источник
INC
операцию сборки , базовые операции ЦП все еще в три раза и требуют блокировки для обеспечения безопасности потока. Хорошая точка зрения. Несмотря на то, чтоINC/DEC
команды могут быть помечены атомарно в сборке и могут быть 1 атомарной операцией.