Разница между изменчивым и синхронизированным в Java - PullRequest
203 голосов
/ 19 августа 2010

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

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

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

...

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

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

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

Ответы [ 5 ]

352 голосов
/ 19 августа 2010

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

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

// 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

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

94 голосов
/ 19 августа 2010

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

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

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

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

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

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

18 голосов
/ 18 декабря 2015

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

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

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

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

enter image description here

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

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

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

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

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

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

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

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

2 голосов
/ 08 февраля 2019

tl; dr :

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

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

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

3) Оптимизация компилятора и ЦП

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

Рассмотрим следующий код:

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

Обратите внимание, что до Java 5Новая модель памяти, энергозависимая, не решила эту проблему.

2 голосов
/ 16 июня 2018

Мне нравится объяснение jenkov

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

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

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

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

enter image description here

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

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

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

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

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

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

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

enter image description here

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

...