Важно понимать, что есть два аспекта безопасности потоков.
- контроль выполнения и
- видимость памяти
Первый связан с контролем, когда код выполняется (включая порядок выполнения инструкций) и может ли он выполняться одновременно, а второй - с тем, когда эффекты в памяти того, что было сделано, видны другим потокам.,Поскольку каждый ЦП имеет несколько уровней кэша между ним и основной памятью, потоки, работающие на разных ЦП или ядрах, могут видеть «память» по-разному в любой момент времени, поскольку потокам разрешено получать и работать с частными копиями основной памяти.
Использование 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 - для общего, но неизменного объекта, воссоздаемого на лету, когда многие другие потоки обращаются к объекту в определенный момент их цикла выполнения.Нужно, чтобы другие потоки начали использовать воссозданный объект после публикации, но ему не нужны дополнительные накладные расходы на полную синхронизацию, сопутствующие конфликты и очистка кэша.
// 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.
Говоря с вашим read-update-напишите вопрос, конкретно.Рассмотрим следующий небезопасный код:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
Теперь, когда метод updateCounter () не синхронизирован, два потока могут вводить его одновременно.Среди множества вариантов того, что может произойти, одна из них заключается в том, что thread-1 выполняет тест для counter == 1000, находит его верным и затем приостанавливается.Затем thread-2 выполняет тот же тест, а также видит его верным и приостанавливается.Затем поток-1 возобновляет работу и устанавливает счетчик на 0. Затем поток-2 возобновляет работу и снова устанавливает счетчик на 0, поскольку он пропустил обновление из потока-1.Это также может произойти, даже если переключение потоков происходит не так, как я описал, а просто потому, что две разные кэшированные копии счетчика присутствовали в двух разных ядрах ЦП, и каждый из потоков работал на отдельном ядре.В этом отношении один поток может иметь счетчик с одним значением, а другой - с каким-то совершенно другим значением только из-за кэширования.
В этом примере важно то, что переменная counter была прочитана из основной памяти в кеш, обновлена в кеше и записана обратно в основную память только в какой-то неопределенный момент позже, когда возник барьер памяти или когда кеш-память была нужна для чего-то еще.Установка счетчика volatile
недостаточна для обеспечения безопасности потока этого кода, потому что проверка на максимум и присвоения являются дискретными операциями, включая приращение, которое представляет собой набор неатомарных read+increment+write
машинных инструкций, что-то вроде:
MOV EAX,counter
INC EAX
MOV counter,EAX
Изменчивые переменные полезны только тогда, когда все выполняемые над ними операции являются «атомарными», как, например, мой пример, когда ссылка на полностью сформированный объект толькочитать или писать (и, как правило, обычно это пишется только из одной точки).Другим примером может быть изменчивая ссылка на массив, поддерживающая список копирования при записи, при условии, что массив был прочитан только при первом получении локальной копии ссылки на него.