Difference between volatile and synchronized in Java

I am wondering at the difference between declaring a variable as volatile and always accessing the variable in a synchronized(this) block in Java?

According to this article http://www.javamex.com/tutorials/synchronization_volatile.shtml there is a lot to be said and there are many differences but also some similarities.

I am particularly interested in this piece of info:

...

  • access to a volatile variable never has the potential to block: we're only ever doing a simple read or write, so unlike a synchronized block we will never hold on to any lock;
  • because accessing a volatile variable never holds a lock, it is not suitable for cases where we want to read-update-write as an atomic operation (unless we're prepared to "miss an update");

What do they mean by read-update-write? Isn't a write also an update or do they simply mean that the update is a write that depends on the read?

Most of all, when is it more suitable to declare variables volatile rather than access them through a synchronized block? Is it a good idea to use volatile for variables that depend on input? For instance, there is a variable called render that is read through the rendering loop and set by a keypress event?

222
задан Albus Dumbledore 16 February 2018 в 09:11
поделиться

2 ответа

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

  1. контроль выполнения и
  2. видимость памяти

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

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

Использование 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). Для целей видимости каждый доступ к полю volatile действует как половина синхронизации.

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

-- JSR 133 (Java Memory Model) FAQ

Итак, теперь обе формы барьера памяти (в текущем 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() несинхронизирован, два потока могут войти в него одновременно. Среди множества вариантов того, что может произойти, один из них заключается в том, что поток-1 выполняет тест на значение counter==1000 и считает его истинным, после чего приостанавливается. Затем поток-2 выполняет тот же тест, также считает его истинным и приостанавливается. Затем поток-1 возобновляет работу и устанавливает счетчик в 0. Затем поток-2 возобновляет работу и снова устанавливает счетчик в 0, потому что он пропустил обновление от потока-1. Это может произойти даже в том случае, если переключение потоков происходит не так, как я описал, а просто потому, что две разные кэшированные копии счетчика находятся в двух разных ядрах процессора, а потоки работают каждый на своем ядре. Иначе говоря, в одном потоке счетчик может иметь одно значение, а в другом - совершенно другое, просто из-за кэширования.

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

MOV EAX,counter
INC EAX
MOV counter,EAX

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

366
ответ дан 23 November 2019 в 04:01
поделиться

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, для потока1 иметь значение 2 для i1 и для потока2 иметь значение 3 для i1, если поток1 и поток2 оба обновили i1, но это обновленное значение еще не было передано в "основную" память или другим потокам.

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

Есть два различия между переменными volitile и synchronized.

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

  1. Поток приобретает блокировку монитора для объекта this.
  2. Поток очищает память от всех своих переменных, т.е. все его переменные эффективно считываются из "основной" памяти.
  3. Выполняется блок кода (в данном случае устанавливая возвращаемое значение на текущее значение i3, которое, возможно, только что было сброшено из "основной" памяти).
  4. (Любые изменения переменных обычно записываются в "основную" память, но для geti3() у нас нет изменений.)
  5. Поток освобождает блокировку монитора для объекта this.

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

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

95
ответ дан 23 November 2019 в 04:01
поделиться
Другие вопросы по тегам:

Похожие вопросы: