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

233

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

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

Я особенно заинтересован в этой части информации:

...

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

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

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

Альбус Дамблдор
источник

Ответы:

383

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

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

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

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

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

Лоуренс Дол
источник
5
Огромное спасибо! Пример со счетчиком прост для понимания. Однако, когда вещи становятся реальностью, это немного по-другому.
Альбус Дамблдор
«С практической точки зрения, на современном оборудовании это обычно вызывает сброс кэшей ЦП при получении монитора и запись в основную память при его освобождении, оба из которых дороги (условно говоря)». , Когда вы говорите, что кеши CPU, это то же самое, что Java Stacks локально для каждого потока? или у потока есть собственная локальная версия Heap? Прошу прощения, если я глупый здесь.
NishM
1
@nishm Это не то же самое, но он будет включать в себя локальные кэши потоков. ,
Лоуренс Дол
1
@ MarianPaździoch: приращение или уменьшение - это НЕ чтение или запись, это чтение и запись; это чтение в регистр, затем инкремент регистра, затем запись в память. Чтение и запись являются индивидуально атомарными, но множественных таких операций нет.
Лоуренс Дол
2
Таким образом, согласно FAQ, не только действия, выполненные после получения блокировки, становятся видимыми после разблокировки, но и все действия, выполняемые этим потоком, становятся видимыми. Даже действия, совершенные до приобретения замка.
Лий
97

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

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

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

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

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

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

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

Керем Байдоган
источник
35
-1, Volatile не получает блокировку, он использует базовую архитектуру ЦП для обеспечения видимости во всех потоках после записи.
Майкл Баркер
Стоит отметить, что в некоторых случаях блокировка может использоваться для гарантии атомарности записей. Например, запись long на 32-битной платформе, которая не поддерживает расширенные права на ширину. Intel избегает этого, используя регистры SSE2 (шириной 128 бит) для обработки изменчивых длин. Однако рассмотрение volatile в качестве блокировки может привести к неприятным ошибкам в вашем коде.
Майкл Баркер
2
Важная семантика, разделяемая блокировками изменчивых переменных, заключается в том, что они оба предоставляют ребра Happens-Before (Java 1.5 и более поздние версии). Вход в синхронизированный блок, снятие блокировки и чтение из volatile все рассматриваются как «приобретение», а снятие блокировки, выход из синхронизированного блока и запись в volatile - все это формы «освобождения».
Майкл Баркер
20

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

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

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

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

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

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

Лоуренс Дол Клири объяснил read-write-update query.

Что касается других ваших запросов

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

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

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

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

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

Равиндра Бабу
источник
Таким образом, чтение может происходить в одно и то же время, и все потоки будут читать последнее значение, потому что ЦП не кэширует основную память в кеш потоков ЦП, но как насчет записи? Запись не должна быть правильной одновременно? Второй вопрос: если блок синхронизирован, но переменная не является изменчивой, значение переменной в синхронизированном блоке все еще может быть изменено другим потоком в другом блоке кода, верно?
the_prole
11

тл; др :

Есть 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. Таким образом, это решает проблему состояния гонки.

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

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

Давид Рафаэли
источник
1
«Хотя это может выглядеть как одна операция, на самом деле это 3: чтение текущего значения x из памяти, добавление 1 к нему и сохранение его обратно в память». - Правильно, потому что значения из памяти должны проходить через схему ЦП для добавления / изменения. Несмотря на то, что это просто превращается в одну INCоперацию сборки , базовые операции ЦП все еще в три раза и требуют блокировки для обеспечения безопасности потока. Хорошая точка зрения. Несмотря на то, что INC/DECкоманды могут быть помечены атомарно в сборке и могут быть 1 атомарной операцией.
Зомби
@Zombies, поэтому, когда я создаю синхронизированный блок для x ++, он превращает его в отмеченный атомарный INC / DEC или использует обычную блокировку?
Давид Рафаэли
Я не знаю! Что я знаю, так это то, что INC / DEC не являются атомарными, потому что для CPU он должен загрузить значение и ПРОЧИТАТЬ его, а также ЗАПИШИТЕ его (в память), как любая другая арифметическая операция.
Зомби