Важно понимать, что есть два аспекта безопасности потоков.
- контроль исполнения и
- видимость памяти
Первый связан с управлением, когда код выполняется (включая порядок выполнения инструкций) и может ли он выполняться одновременно, а второй - с тем, когда эффекты в памяти того, что было сделано, видны другим потокам. Поскольку каждый ЦП имеет несколько уровней кэша между ним и основной памятью, потоки, работающие на разных ЦП или ядрах, могут видеть «память» по-разному в любой момент времени, поскольку потокам разрешено получать и работать с частными копиями основной памяти.
Использование не 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
Изменчивые переменные полезны только тогда, когда все операции, выполняемые над ними, являются «атомарными», как, например, в моем примере, когда ссылка на полностью сформированный объект только для чтения или записи (и, как правило, обычно она пишется только из одной точки). Другим примером может быть изменчивая ссылка на массив, поддерживающая список копирования при записи, при условии, что массив был прочитан только при первом получении локальной копии ссылки на него.