Летучие против атомных
Я читаю где-то ниже строки.
Ключевое слово Java volatile не означает атомарное, его распространенное заблуждение, что после объявления volatile,
++
операция будет атомарной, чтобы сделать операцию атомарной, вам все равно нужно обеспечить эксклюзивный доступ с помощьюsynchronized
метод или блок в Java.
Так что же произойдет, если два потока атакуют volatile
примитивная переменная одновременно?
Означает ли это, что тот, кто захватывает его, первым установит его значение. И если между тем какой-то другой поток подойдет и прочитает старое значение, в то время как первый поток изменял его значение, то не будет ли новый поток читать старое значение?
В чем разница между атомным и изменчивым ключевым словом?
6 ответов
Эффект от volatile
Ключевым словом является то, что каждая отдельная операция чтения или записи для этой переменной является атомарной.
Примечательно, однако, что операция, которая требует более одного чтения / записи - например, i++
, что эквивалентно i = i + 1
, который делает одно чтение и одну запись - не атомарный, так как другой поток может писать в i
между чтением и записью.
Atomic
классы, как AtomicInteger
а также AtomicReference
, предоставлять более широкий спектр операций атомарно, в частности, включая приращение для AtomicInteger
,
Volatile и Atomic - это две разные концепции. Volatile гарантирует, что определенное ожидаемое состояние (памяти) верно для разных потоков, в то время как Atomics гарантирует, что операции с переменными выполняются атомарно.
Возьмите следующий пример двух потоков в Java:
Тема А:
value = 1;
done = true;
Нить Б:
if (done)
System.out.println(value);
Начиная с value = 0
а также done = false
правило многопоточности говорит нам, что не определено, будет ли Thread B печатать значение. Кроме того, значение не определено в этой точке! Чтобы объяснить это, вам нужно немного узнать об управлении памятью Java (что может быть сложным), вкратце: потоки могут создавать локальные копии переменных, а JVM может переупорядочивать код для его оптимизации, поэтому нет гарантии, что приведенный выше код запускается именно в таком порядке. Установка true в значение, а затем установка значения 1 может быть возможным результатом оптимизации JIT.
volatile
только гарантирует, что в момент доступа к такой переменной новое значение будет сразу же видно всем другим потокам, а порядок выполнения гарантирует, что код находится в том состоянии, в котором вы ожидаете его получить. Так что в случае кода выше, определяя done
как volatile будет гарантировать, что всякий раз, когда Поток B проверяет переменную, это либо ложь, либо истина, и если это правда, то value
также был установлен в 1.
В качестве побочного эффекта от volatile значение такой переменной задается атомно во всей нити (при очень незначительной стоимости выполнения). Однако это важно только в 32-разрядных системах, в которых iE используют длинные (64-разрядные) переменные (или аналогичные), в большинстве случаев установка / чтение переменной в любом случае является атомарной. Но есть важное различие между атомарным доступом и атомарной операцией. Volatile обеспечивает только атомарный доступ, а Atomics - атомарный.
Возьмите следующий пример:
i = i + 1;
Независимо от того, как вы определяете i, другой поток, считывающий значение только при выполнении вышеуказанной строки, может получить i или i + 1, потому что операция выполняется не атомарно. Если другой поток устанавливает для i другое значение, в худшем случае я мог бы вернуть его обратно тому, что было раньше, потоком A, потому что это было только в середине вычисления i + 1 на основе старого значения, а затем установить i снова к этому старому значению + 1. Объяснение:
Assume i = 0
Thread A reads i, calculates i+1, which is 1
Thread B sets i to 1000 and returns
Thread A now sets i to the result of the operation, which is i = 1
Атомика, такая как AtomicInteger, гарантирует, что такие операции происходят атомарно. Таким образом, вышеупомянутая проблема не может произойти, я бы либо 1000 или 1001, как только оба потока завершены.
В среде многопоточности есть два важных понятия.
- валентность
- видимость
Volatile
устраняет проблему видимости, но не имеет отношения к атомарности. Volatile
помешает компилятору переупорядочить инструкцию, которая включает запись и последующее чтение изменчивой переменной. например k++
Вот k++
это не одна машинная инструкция, скорее это три машинные инструкции.
- скопировать значение для регистрации
- увеличить его
- положить его обратно
Таким образом, даже если вы объявляете переменную volatile
это не сделает эту операцию атомарной, что означает, что другой поток может видеть промежуточный результат, который является устаревшим или нежелательным значением для другого потока.
Но AtomicInteger
, AtomicReference
основаны на инструкции сравнения и обмена. CAS имеет три операнда: ячейка памяти V
на котором оперировать, ожидаемое старое значение A
и новое значение B
, CAS
атомно обновляет V
к новому значению B
, но только если значение в V
соответствует ожидаемому старому значению A
; в противном случае это ничего не делает. В любом случае, он возвращает значение в настоящее время в V
, Это используется JVM в AtomicInteger
, AtomicReference
и они называют функцию как compareAndSet()
, Если эта функциональность не поддерживается базовым процессором, JVM реализует ее с помощью спин-блокировки.
Как и пытался, как указано, volatile
имеет дело только с видимостью.
Рассмотрим этот фрагмент в параллельной среде:
boolean isStopped = false;
:
:
while (!isStopped) {
// do some kind of work
}
Идея в том, что какой-то поток может изменить значение isStopped
от false до true, чтобы указать последующему циклу, что пора прекратить цикл.
Интуитивно понятно, что проблем нет. Логично, если другой поток делает isStopped
равным true, тогда цикл должен завершиться. Реальность такова, что цикл, вероятно, никогда не прекратится, даже если другой поток делает isStopped
равно истине.
Причина этого не интуитивна, но учтите, что современные процессоры имеют несколько ядер и что каждое ядро имеет несколько регистров и несколько уровней кэш-памяти, которые недоступны для других процессоров. Другими словами, значения, которые кэшируются в локальной памяти одного процессора, не видны потокам, выполняющимся на другом процессоре. В этом заключается одна из центральных проблем параллелизма: видимость.
Модель памяти Java не дает никаких гарантий относительно того, когда изменения, внесенные в переменную в потоке, могут стать видимыми для других потоков. Чтобы гарантировать, что обновления видны, как только они сделаны, необходимо выполнить синхронизацию.
volatile
Ключевое слово является слабой формой синхронизации. Хотя он ничего не делает для взаимного исключения или атомарности, он обеспечивает гарантию того, что изменения, внесенные в переменную в одном потоке, станут видимыми для других потоков, как только они будут сделаны. Поскольку отдельные операции чтения и записи в переменные, которые не являются 8-байтовыми, являются атомарными в Java, объявляя переменные volatile
обеспечивает простой механизм для обеспечения видимости в ситуациях, когда нет других требований атомарности или взаимного исключения.
volatile
используется ключевое слово:
- сделать неатомарные 64-битные операции атомарными:
long
а такжеdouble
, (все другие, примитивные обращения уже гарантированно будут атомарными!) - чтобы обновления переменных гарантированно были видны другим потокам + эффекты видимости: после записи в энергозависимую переменную все переменные, которые были видимы перед записью этой переменной, становятся видимыми для другого потока после чтения той же энергозависимой переменной (произойдет до упорядочения).
java.util.concurrent.atomic.*
классы, в соответствии с документами Java:
Небольшой инструментарий классов, которые поддерживают поточно-ориентированное программирование без блокировок для отдельных переменных. По сути, классы в этом пакете расширяют понятие изменчивых значений, полей и элементов массива до тех, которые также обеспечивают элементарную операцию условного обновления формы:
boolean compareAndSet(expectedValue, updateValue);
Атомные классы построены вокруг атомных compareAndSet(...)
функция, которая отображается на атомарную инструкцию процессора. Атомные классы вводят порядок " произойдет до" как volatile
переменные делают. (с одним исключением: weakCompareAndSet(...)
).
Из документов Java:
Когда поток видит обновление атомарной переменной, вызванное weakCompareAndSet, он не обязательно видит обновления каких-либо других переменных, которые произошли до слабого CompareAndSet.
На ваш вопрос:
Означает ли это, что тот, кто захватывает его, первым установит его значение. И если между тем какой-то другой поток подойдет и прочитает старое значение, в то время как первый поток изменял свое значение, то не будет ли новый поток читать старое значение?
Вы ничего не блокируете, то, что вы описываете, является типичным условием гонки, которое в конечном итоге произойдет, если потоки получат доступ к общим данным без надлежащей синхронизации. Как уже упоминалось, объявление переменной volatile
в этом случае будет только гарантировать, что другие потоки увидят изменение переменной (значение не будет кэшировано в регистре некоторого кэша, который виден только одному потоку).
В чем разница между
AtomicInteger
а такжеvolatile int
?
AtomicInteger
обеспечивает атомарные операции на int
с правильной синхронизацией (например, incrementAndGet()
, getAndAdd(...)
...), volatile int
просто обеспечит видимость int
в другие темы.
Так что же произойдет, если два потока атакуют переменную примитивной переменной одновременно?
Обычно каждый из них может увеличивать значение. Однако, когда-нибудь, оба обновят значение одновременно, и вместо увеличения на 2, будет добавлено и увеличение потока на 1, и только 1.
Означает ли это, что тот, кто захватывает его, первым установит его значение.
Там нет блокировки. Что это synchronized
для.
И если между тем какой-то другой поток подойдет и прочитает старое значение, в то время как первый поток изменял свое значение, то не будет ли новый поток читать старое значение?
Да,
В чем разница между атомным и изменчивым ключевым словом?
AtomicXxxx заключает в себе переменные, поэтому они в основном одинаковы, разница в том, что они предоставляют операции более высокого уровня, такие как CompareAndSwap, который используется для реализации приращения.
AtomicXxxx также поддерживает lazySet. Это похоже на энергозависимый набор, но не останавливает конвейер, ожидающий завершения записи. Это может означать, что если вы читаете только что записанное значение, вы можете увидеть старое значение, но вам все равно не следует этого делать. Разница в том, что установка volatile занимает около 5 нс, бит lazySet - около 0,5 нс.