Разница между изменчивой и синхронизированной в Java

Меня интересует разница между объявлением переменной как volatile и постоянным доступом к переменной в блоке synchronized(this) в Java?

В соответствии с этой статьей http://www.javamex.com/tutorials/synchronization_volatile.shtml можно много чего сказать, есть много различий, но есть и некоторые сходства.

Мне особенно интересна эта информация:

...

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

Что они подразумевают под чтение-обновление-запись? Разве запись также не является обновлением или они просто означают, что обновление является записью, которая зависит от чтения?

Прежде всего, когда лучше объявить переменные volatile, а не обращаться к ним через блок synchronized? Является ли хорошей идеей использовать volatile для переменных, которые зависят от ввода? Например, есть переменная с именем render, которая читается через цикл рендеринга и устанавливается событием нажатия клавиши?

Ответ 1

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

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

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

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

Использование 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 Memory Model) FAQ

Итак, теперь обе формы барьера памяти (в соответствии с текущим 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.

Говоря, в частности, на ваш вопрос о чтении, обновлении и записи. Рассмотрим следующий небезопасный код:

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

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

Ответ 2

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

Существует два различия между волютильными и синхронизированными.

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

  • Нить получает блокировку на мониторе для этого объекта.
  • В памяти потока сбрасываются все его переменные, т.е. все его переменные эффективно считываются из "основной" памяти.
  • Выполняется блок кода (в этом случае устанавливается значение возврата к текущему значению i3, которое могло быть только reset из основной памяти).
  • (Любые изменения переменных обычно записываются в "главную" память, но для geti3() у нас нет изменений.)
  • Этот поток освобождает блокировку на мониторе для объекта.

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

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

Ответ 3

synchronized - это модификатор ограничения уровня уровня уровня/уровня блока. Он будет следить за тем, чтобы один поток владел блокировкой для критической секции. Только поток, которому принадлежит блокировка, может войти в блок synchronized. Если другие потоки пытаются получить доступ к этому критическому разделу, им приходится ждать, пока текущий владелец не выпустит блокировку.

volatile - модификатор с переменным доступом, который заставляет все потоки получать последнее значение переменной из основной памяти. Для доступа к переменным volatile не требуется блокировка. Все потоки могут одновременно получить доступ к изменчивой переменной.

Хороший пример использования переменной volatile: переменная Date.

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

введите описание изображения здесь

Посмотрите на статью для лучшего понимания концепции volatile.

Лоуренс Доль расслабился, объяснил ваш read-write-update query.

Относительно ваших других запросов

Когда более целесообразно объявлять переменные волатильными, чем доступ к ним через синхронизацию?

Вы должны использовать volatile, если считаете, что все потоки должны получать фактическое значение переменной в реальном времени, как пример, который я объяснил для переменной Date.

Полезно ли использовать volatile для переменных, которые зависят от ввода?

Ответ будет таким же, как в первом запросе.

Обратитесь к этой статье для лучшего понимания.

Ответ 4

  • volatile ключевое слово в java является модификатором поля, а synchronized изменяет кодовые блоки и методы.

  • synchronized получает и освобождает блокировку на мониторах. Ключевое слово java volatile не требует этого.

  • Темы в Java могут быть заблокированы для ожидания любого монитора в случае synchronized, это не относится к ключевому слову volatile в Java.

  • synchronized метод влияет на производительность больше, чем volatile ключевое слово в Java.

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

  • Вы не можете синхронизировать нулевой объект, но ваша переменная volatile в java может быть нулевой.

  • Из Java 5 Запись в поле volatile имеет тот же эффект памяти, что и релиз монитора, а чтение из энергозависимого поля имеет тот же эффект памяти, что и монитор.

Ответ 5

Мне нравится Дженков объяснение. Многопоточность.

Видимость общих объектов

Если два или более потоков совместно используют объект без надлежащего использования volatile объявлений или синхронизации, обновления общего объекта, сделанные одним потоком, могут быть невидимы для других потоков.

Представьте, что общий объект изначально хранится в основной памяти. Поток, работающий на ЦП, затем считывает общий объект в свой кэш ЦП. Там он вносит изменения в общий объект. Пока кэш ЦП не был сброшен обратно в основную память, измененная версия общего объекта не видна потокам, работающим на других ЦП. Таким образом, каждый поток может получить свою собственную копию общего объекта, каждая копия будет находиться в отдельном кэше ЦП.

Следующая диаграмма иллюстрирует набросок ситуации. Один поток, работающий на левом ЦП, копирует общий объект в кэш своего ЦП, и изменяет его переменную count на 2. Это изменение невидимо для других потоков, работающих на правом ЦП, поскольку обновление для счетчика имеет еще не был возвращен в основную память.

Летучие[About]

Для решения этой проблемы вы можете использовать ключевое слово Java volatile. volatile keyword может убедиться, что:

  1. данная переменная считывается непосредственно из основной памяти и всегда записывается обратно в основную память при обновлении.
  2. происходит-прежде.

Условия гонки

Если два или более потоков совместно используют объект и более одного потока обновляют переменные в этом общем объекте, может возникнуть race conditions.

Представьте, что thread A считывает переменную count общего объекта в его кэш-память ЦП. Представьте также, что thread B делает то же самое, но в другой кэш процессора. Теперь thread A добавляет один к счету, а thread B делает то же самое. Теперь var1 был увеличен два раза, один раз в каждом кэше ЦП.

Если бы эти приращения были выполнены последовательно, счетчик переменных был бы увеличен в два раза и имел бы исходное значение + 2, записанное обратно в основную память.

Тем не менее, два приращения были выполнены одновременно без надлежащей синхронизации. Независимо от того, какой из потоков A и B, который записывает свою обновленную версию count в основную память, обновленное значение будет только на 1 больше исходного значения, несмотря на два приращения.

Эта диаграмма иллюстрирует возникновение проблемы с условиями гонки, как описано выше:

Синхронизированные[About]

Для решения этой проблемы вы можете использовать synchronized

  1. Синхронизированный блок гарантирует, что только один поток может ввести данный критический раздел кода в любой момент времени.
  2. происходит-прежде.

Связанная тема Сравнить и поменять местами

Ответ 6

ТЛ; др:

Существует три основных проблемы с многопоточностью:

1) Условия гонки

2) Кеширование/устаревшая память

3) оптимизация Complier и CPU

volatile может решить 2 & 3, но не может решить 1. synchronized/явные блокировки могут решить 1, 2 & 3.

Разработка:

1) Считать этот поток небезопасным кодом:

x++;

Хотя это может выглядеть как одна операция, на самом деле это 3: чтение текущего значения x из памяти, добавление 1 к нему и сохранение его обратно в память. Если несколько потоков пытаются сделать это одновременно, результат операции не определен. Если x изначально был 1, то после двух потоков, работающих с кодом, может быть 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 выполняется перед установкой b в значение true), поскольку b устанавливается в true только после установки x 20, но компилятор/ЦП может принять решение изменить порядок потока A, в этом случае threadB также может вывести 10. Пометка b как volatile гарантирует, что он не будет переупорядочен (или исключен в некоторых случаях). Что означает, что threadB может печатать только 20 (или вообще ничего). Маркировка методов как синхронизированных приведет к тому же результату. Также пометка переменной как volatile только гарантирует, что она не будет переупорядочена, но все до/после нее все еще может быть переупорядочено, поэтому синхронизация может быть более подходящей в некоторых сценариях.

Обратите внимание, что до появления новой модели памяти Java 5 volatile не решала эту проблему.