Ответ 1
Как объяснено в документации пакета для атомистики (в общем, не для уточнений):
Эффекты памяти для доступа и обновлений атоматики обычно следуют правилам для летучих, [...]:
get
имеет эффект памяти при чтении переменнойvolatile
.set
имеет эффекты памяти записи (назначения) переменнойvolatile
.- [...]
compareAndSet
и все другие операции чтения и обновления, такие какgetAndIncrement
, имеют эффекты памяти как для чтения, так и для записи переменныхvolatile
.
Какую проблему пытается решить атомный compareAndSet
? Зачем использовать (например) atomicInteger.compareAndSet(1,2)
вместо if(volatileInt == 1) { volatileInt = 2; }
? Он не пытается решить какую-либо проблему при одновременном чтении, потому что к ним уже позабочен обычный volatile
. ( "Неустойчивое" чтение или запись совпадает с "атомарным" чтением или записью. Одновременное чтение было бы проблемой только в том случае, если это произошло в середине записи, или если инструкции были переупорядочены или оптимизированы каким-то проблемным образом; но volatile
уже предотвращает эти вещи.) Единственная проблема, которую решает compareAndSet
, заключается в том, что в подходе volatileInt
может появиться и другой поток с одновременной записью, когда мы читаем volatileInt
(volatileInt == 1
) и когда мы напишем ему (volatileInt = 2
). compareAndSet
решает эту проблему, блокируя любые конкурирующие записи за это время.
Это одинаково верно в конкретном случае "updaters" (AtomicReferenceFieldUpdater
и т.д.): volatile
чтения по-прежнему просто персиковые. Единственным ограничением методов compareAndSet
updaters является то, что вместо "блокировки любых конкурирующих записей", как я писал выше, они блокируют только конкурирующие записи из того же экземпляра AtomicReferenceFieldUpdater
; они не могут защитить вас, когда вы одновременно обновляете поле volatile
напрямую (или, если на то пошло, когда вы одновременно используете несколько AtomicReferenceFieldUpdater
для обновления того же поля volatile
). (Кстати, в зависимости от того, как вы смотрите на это: то же самое относится к AtomicReference
и его родственникам: если бы вы обновляли свои поля таким образом, чтобы обойти свои собственные сеттеры, они не могли защитить вас. a AtomicReference
действительно владеет своим полем, а это private
, поэтому нет необходимости предупреждать вас о том, чтобы каким-то образом его модифицировать внешними средствами.)
Итак, чтобы ответить на ваш вопрос: да, вы можете продолжать читать поля volatile
с теми же гарантиями атомарности против частичных/несогласованных чтений, переупорядочиваемых операторов и т.д.
Отредактировано для добавления (6 декабря): Любой, кто особенно заинтересован в этом вопросе, вероятно, будет заинтересован в обсуждении сразу же. Меня попросили уточнить ответ, чтобы уточнить важные моменты из этого обсуждения:
-
Я думаю, что наиболее важным моментом для добавления является то, что выше это моя собственная интерпретация документации. Я достаточно уверен, что понял это правильно и что никакая другая интерпретация не имеет смысла; и я могу, при желании, аргументировать точку в длину;-); но ни я, ни кто-либо еще не приводили никаких ссылок на какой-либо авторитетный документ, который более подробно затрагивает этот вопрос, чем два документа, упомянутых в самом вопросе (класс Javadoc и Java Concurrency на практике), и один документ, упомянутый в моем оригинале ответьте на него выше (пакет Javadoc).
-
Следующим наиболее важным моментом, я думаю, является то, что, хотя в документации для
AtomicReferenceUpdater
говорится, что небезопасно смешиватьcompareAndSet
с volatile write, я считаю, что на типичных платформах это действительно безопасно. Это небезопасно только в общем случае. Я говорю об этом из-за следующего комментария из документации пакета:Спецификации этих методов позволяют реализациям использовать эффективные атомные инструкции на уровне машины, доступные на современных процессорах. Однако на некоторых платформах поддержка может повлечь за собой некоторую форму внутренней блокировки. Таким образом, методы строго не гарантируются как неблокирующие - поток может временно блокироваться перед выполнением операции.
Итак:
- В типичной реализации JDK для современного процессора
AtomicReference.set
просто использует volatile write, так какAtomicReference.compareAndSet
использует операцию сравнения и замены, которая является атомарной относительно летучих записей.AtomicReferenceUpdater.set
обязательно более сложный, чемAtomicReference.set
, потому что он должен использовать рефлексивную логику для обновления поля в другом объекте, но я утверждаю, что это единственная причина, по которой это сложнее. Типичная реализация вызываетUnsafe.putObjectVolatile
, которая является volatile write более длинным именем. - Но не все платформы поддерживают этот подход, и если они этого не делают, блокировка разрешена. Рискуя упростить, я полагаю, что это означает, что атомный класс
compareAndSet
может быть реализован (более или менее) с применениемsynchronized
к методу, который используетget
иset
прямо. Но для этого,set
также должен бытьsynchronized
, по причине, описанной в моем первоначальном ответе выше; то есть он не может быть просто volatile write, потому что тогда он может изменить поле после того, какcompareAndSet
вызвалget
, но доcompareAndSet
вызываетset
. - Излишне говорить, что мой оригинальный ответ использования фразы "блокировка" не следует воспринимать буквально, так как на типичной платформе не возникает ничего, что связано с блокировкой.
- В типичной реализации JDK для современного процессора
-
В Sun JDK 1.6.0_05 реализации
java.util.concurrent.ConcurrentLinkedQueue<E>
мы находим следующее:private static class Node<E> { private volatile E item; private volatile Node<E> next; private static final AtomicReferenceFieldUpdater<Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next"); private static final AtomicReferenceFieldUpdater<Node, Object> itemUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Object.class, "item"); Node(E x) { item = x; } Node(E x, Node<E> n) { item = x; next = n; } E getItem() { return item; } boolean casItem(E cmp, E val) { return itemUpdater.compareAndSet(this, cmp, val); } void setItem(E val) { itemUpdater.set(this, val); } Node<E> getNext() { return next; } boolean casNext(Node<E> cmp, Node<E> val) { return nextUpdater.compareAndSet(this, cmp, val); } void setNext(Node<E> val) { nextUpdater.set(this, val); } }
(примечание: пробелы, скорректированные на компактность), где после создания экземпляра нет волатильных записей — то есть все записи выполняются через
AtomicReferenceFieldUpdater.compareAndSet
илиAtomicReferenceFieldUpdater.set
— но волатильные чтения, по-видимому, используются свободно, без единого вызоваAtomicReferenceFieldUpdater.get
. Более поздние выпуски JDK 1.6 были изменены для непосредственного использованияUnsafe
(это произошло с Oracle JDK 1.6.0_27), но обсуждения в списке рассылки JSR 166 связывают это изменение с соображениями производительности, а не с какими-либо сомнениями относительно правильности предыдущего реализация.- Но я должен отметить, что это не пуленепробиваемый авторитет. Для удобства я пишу о реализации Sun, как будто это было унитарным, но моя предыдущая маркерная точка делает очевидным, что реализации JDK для разных платформ, возможно, придется делать по-другому. Вышеприведенный код кажется мне написанным на платформе нейтральным способом, поскольку он избегает простой волатильной записи в пользу вызовов
AtomicReferenceFieldUpdater.set
; но кто-то, кто не согласен с моей интерпретацией одной точки, может не согласиться с моей интерпретацией другой и может утверждать, что вышеуказанный код не предназначен для всех платформ. - Еще одна слабость этого авторитета заключается в том, что, хотя
Node
, похоже, позволяет волатильным чтениям проходить одновременно с вызовамиAtomicReferenceFieldUpdater.compareAndSet
, это частный класс; и я не предпринял никаких доказательств того, что его владелец (ConcurrentLinkedQueue
) фактически совершает такие звонки без своих собственных мер предосторожности. (Но, хотя я не доказал свою претензию, я сомневаюсь, что кто-нибудь будет оспаривать ее.)
- Но я должен отметить, что это не пуленепробиваемый авторитет. Для удобства я пишу о реализации Sun, как будто это было унитарным, но моя предыдущая маркерная точка делает очевидным, что реализации JDK для разных платформ, возможно, придется делать по-другому. Вышеприведенный код кажется мне написанным на платформе нейтральным способом, поскольку он избегает простой волатильной записи в пользу вызовов
Пожалуйста, ознакомьтесь с нижеприведенными комментариями для фона по этому добавлению и для дальнейшего обсуждения.