Влияет ли volatile на неизменяемые переменные?

Хорошо, предположим, у меня есть куча переменных, одна из которых объявлена ​​volatile:

int a;
int b;
int c;
volatile int v;

Если один поток записывает во все четыре переменные (запись в v в последнюю очередь), а другой поток читает из всех четырех переменных (сначала читает из v), увидит ли этот второй поток значения, записанные в a, b и c первым потоком, даже хотя сами они не объявлены летучими? Или он может видеть устаревшие значения?

Так как, кажется, есть некоторая путаница: я не пытаюсь намеренно сделать что-то небезопасное. Я просто хочу понять модель памяти Java и семантику ключевого слова volatile. Чистое любопытство.


person fredoverflow    schedule 29.05.2011    source источник
comment
FredOverflow, если вам нужен энергозависимый массив, используйте либо AtomicReferenceArray, либо AtomicIntegerArray, либо просто посмотрите, что предлагает Unsafe. Я отвечу о несвежей части.   -  person bestsss    schedule 29.05.2011
comment
@ Том, почему ненависть к небезопасным (помимо того, что это действительно небезопасно, если вы пишете любой нативный код, небезопасный не хуже)?   -  person bestsss    schedule 29.05.2011
comment
@bestsss Unsafe — это полностью неподдерживаемый API, который вы не должны использовать. Это позволяет тривиально скомпрометировать надежность JVM, даже не покидая Java. Вы должны использовать хаки, чтобы обойти защиту, которая затрудняет доступ.   -  person Tom Hawtin - tackline    schedule 29.05.2011
comment
@Tom, -Xbootclasspath/p - вполне стандартный вариант для получения Unsafe (я знаю, что большинство людей делают это через Reflection), но если я должен полагаться на Unsafe, я бы предпочел поместить туда свой код. Боковое примечание: худшее, что компрометирует java, когда-либо случалось со мной: двойная ошибка сегмента во время ошибки stackoverflow, он выходит из JVM без ЛЮБОЙ одиночной трассировки (кроме syslog), и код на 100% java, никаких хакерских трюков, взял мне 2 недели, чтобы это выяснить и поменять серверное железо.   -  person bestsss    schedule 29.05.2011
comment
Что это за Unsafe и как оно связано с вопросом?   -  person fredoverflow    schedule 29.05.2011
comment
@FredOverflow, это странно, могу поклясться, что там была строчка об атомном массиве (отсюда и мой первый комментарий). Небезопасным является sun.misc.Unsafe, и если вы не знаете, что это такое, то он вам точно не нужен.   -  person bestsss    schedule 29.05.2011
comment
@FredOverflow, нашел, это был ваш первый комментарий к сообщению Тома, и часть симуляции заставила меня подумать, что у вас будет постоянно меняющийся код и т. д. и т. д.   -  person bestsss    schedule 29.05.2011


Ответы (4)


да. volatile, блокировки и т. д. настраивают отношение происходит до, но оно влияет на все переменные (в новой модели памяти Java (JMM) из Java SE 5/JDK 1.4). Отчасти делает его полезным для непримитивных летучих веществ...

person Tom Hawtin - tackline    schedule 29.05.2011
comment
Итак, я мог бы, например, имитировать изменчивые массивы, сначала записывая в массив и записывая в какой-то фиктивный volatile int в одном потоке, а затем читая из этого фиктивного volatile int и читая из массива в другом потоке? - person fredoverflow; 29.05.2011
comment
@FredOverflow Не очень полезно и надежно. volatile сложно работать. - person Tom Hawtin - tackline; 29.05.2011
comment
JDK 1.4 не следует новой модели памяти Java (это из jdk 1.5) - person bestsss; 29.05.2011
comment
@bestsss Любые отклонения от нового JMM были исправлены как ошибки в версии 1.4. Не похоже, чтобы было практично следовать JMM спецификации J2SE 1.4. (Надо было сказать J2SE 5.0, а не Java SE 5.) - person Tom Hawtin - tackline; 29.05.2011
comment
@ Том, я думаю, ты прав насчет части бэкпорта. Я никогда не знал, что он был полностью реализован для его поддержки (помните, что были введены только некоторые исправления) - person bestsss; 29.05.2011
comment
Возможно, это работает, но я надеюсь, что никто никогда не использует этот трюк в реальном коде со многими разработчиками; было бы беспорядок поддерживать правильный параллелизм. - person toto2; 29.05.2011
comment
@toto volatile используется редко. Чтобы иметь какое-либо реальное применение, вам нужно будет комбинировать его с другими операциями чтения/записи. Обычно (в этих редких случаях) они находятся в объектах, на которые ссылается ссылка, присвоенная volatile. Но используются и другие поля, например, это делается при реализации java.util.concurrent, и это единственный способ использовать примитивы volatile с несколькими полями. - person Tom Hawtin - tackline; 29.05.2011
comment
Где сказано (в модели памяти Java), что volatile влияет на все переменные? Насколько я могу судить, это гарантирует только happens-before для конкретной переменной, которая является изменчивой. - person Gili; 08.01.2014
comment
@Gili 17.4.2 определяет чтение и запись переменных как межпотоковое действие. 17.4.5 утверждает, что действия в одном и том же потоке в программном порядке имеют отношение «происходит до». происходит до — частичный порядок. - person Tom Hawtin - tackline; 08.01.2014

Я собираюсь поговорить о том, что, по моему мнению, вас действительно может интересовать совмещение синхронизации.

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

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

Вы не упомянули второе условие, справедливое для вашего примера, но мы все равно можем его изучить. Модель для писателя выглядит следующим образом:

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

читатели работают следующим образом:

  • Читать переменную volatile guard в любое время, и если ее значение соответствует критериям, то
  • Прочтите другие энергонезависимые переменные.

Считыватели не должны считывать другие энергонезависимые переменные, если изменчивая защитная переменная еще не указывает правильное значение.

Защитная переменная действует как ворота. Он закрыт до тех пор, пока писатель не установит для него определенное значение или набор значений, которые все соответствуют критериям, указывающим на то, что ворота теперь открыты. Энергонезависимые переменные охраняются за воротами. Читателю не разрешается читать их, пока ворота не откроются. Как только ворота открыты, читатель увидит непротиворечивое представление набора энергонезависимых переменных.

Обратите внимание, что многократно запускать этот протокол небезопасно. Писатель не может продолжать изменять энергонезависимые переменные после того, как он открыл ворота. В этот момент несколько потоков чтения могут считывать эти другие переменные, и они могут, хотя и не гарантированно, видеть обновления этих переменных. Просмотр некоторых, но не всех этих обновлений приведет к противоречивым представлениям набора.

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

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

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

person seh    schedule 29.05.2011
comment
хорошее резюме, реквизит для полного описания, чтобы описать его. На самом деле я думал о похожем случае, но использовал v как версию и читал ее в начале и в конце охраняемого a/b/c. Данные считаются непротиворечивыми, только если значения совпадают, если нет циклов. По-прежнему требуется одно чтение и запись, хотя и постоянно меняющаяся версия (скажем, volatile long v; v++) - person bestsss; 29.05.2011
comment
Спасибо, бестссс. Использование счетчика, как вы описываете, решает проблему A-B-A, которая возникла бы, если бы вместо этого вы использовали логический флаг, указывающий, активен ли писатель (например, AtomicStampedReference). Тем не менее, я думаю, вам нужно какое-то недопустимое значение, которое позволит читателям отличить ситуацию, когда автор начал обновлять энергонезависимые переменные, но еще не успел изменить номер версии. . Возможно, будет достаточно пары переменных: первая — номер транзакции, а вторая — номер фиксации. Поднимите первое, зафиксируйте, подняв второе. - person seh; 29.05.2011
comment
AtomicMarkableReference не имеет встроенного (на данный момент), и, к сожалению, это очень плохая производительность. Было бы здорово использовать один бит в указателе, увы, это полностью раздутый объект с выделением каждый раз. Я не проверял AtomicStampedReference, но тоже сомневаюсь. Действительно, для должного обеспечения согласованности данных потребуются две переменные. - person bestsss; 29.05.2011
comment
@bestsss, что такого неэффективного в AtomicMarkableReference по сравнению, скажем, с двойной проверкой блокировки? - person Gili; 08.01.2014

видит ли этот второй поток значения, записанные в a, b и c первым потоком, даже если они сами не объявлены изменчивыми? Или он может видеть устаревшие значения?

Вы получите устаревшие чтения, потому что вы не можете гарантировать, что значения a, b, c установлены после чтения v. Использование конечного автомата (но вам нужен CAS для изменения состояния) - это способ решения подобные вопросы, но это выходит за рамки обсуждения.

Возможно, эта часть неясна, после записи в v и чтения сначала из v вы получите правильные результаты (неустаревшие чтения), основная проблема в том, что если вы сделаете if (v==STATE1){...proceed...}, нет гарантии, что какой-то другой поток не будет изменение состояния a/b/c. В этом случае будет чтение состояния. Если вы измените a/b/c+v только один раз, вы получите правильный результат.

Освоение параллелизма и структур без блокировок — действительно сложная задача. У Дуга Ли есть хорошая книга, и большинство докладов/статей доктора Клиффа Кликка — прекрасное богатство, если вам нужно что-то, с чем можно начать копаться.

person bestsss    schedule 29.05.2011
comment
Нет, чтение a, b, c в том же потоке, что и чтение v, приведет к получению значений, установленных до того, как v было записано в другом потоке. (Очевидно, что вы также можете получить значения, записанные позже или из другого потока.) - person Tom Hawtin - tackline; 29.05.2011
comment
@ Том, я понимаю, я отредактировал ответ, так как он выглядит неясным. Основная проблема заключается в том, что любой другой поток может его изменить. - person bestsss; 29.05.2011

Да, изменчивая запись "происходит до" следующего изменчивого чтения той же переменной.

Хотя @seh прав в отношении проблем согласованности с несколькими переменными, есть варианты использования, в которых требуется меньшая согласованность.

Например, поток записи обновляет некоторые переменные состояния; поток чтения отображает их быстро. Между переменными нет большой связи, мы заботимся только о быстром чтении новых значений. Мы могли бы сделать каждую переменную состояния изменчивой. Или мы могли бы использовать только одну изменчивую переменную в качестве защиты видимости.

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

person irreputable    schedule 30.05.2011