В чем разница между атомарным / энергозависимым / синхронизированным?

297

Как атомарные / энергозависимые / синхронизированные работают внутри?

В чем разница между следующими блоками кода?

Код 1

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

Код 2

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

Код 3

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

Работает ли volatileследующим образом? Является

volatile int i = 0;
void incIBy5() {
    i += 5;
}

эквивалентно

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

Я думаю, что два потока не могут войти в синхронизированный блок одновременно ... я прав? Если это правда, то как atomic.incrementAndGet()работает без synchronized? И это потокобезопасно?

И в чем разница между внутренним чтением и записью в переменные / атомарные переменные? В какой-то статье я читал, что в потоке есть локальная копия переменных - что это?

Hardik
источник
5
Это вызывает много вопросов с кодом, который даже не компилируется. Возможно, вам следует прочитать хорошую книгу, например, «Параллелизм Java на практике».
JB Низет
4
@JBNizet ты прав !!! У меня есть эта книга, у нее нет краткой концепции Atomic, и я не получаю некоторые концепции этого. Конечно, это моя ошибка, а не автора.
Hardik
4
Вам не нужно заботиться о том, как это реализовано (и это зависит от ОС). Что вы должны понять, так это контракт: значение увеличивается атомарно, и все остальные потоки гарантированно увидят новое значение.
JB Низет

Ответы:

392

Вы конкретно спрашиваете, как они работают внутри , так что вот вы:

Нет синхронизации

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Он в основном считывает значение из памяти, увеличивает его и возвращает в память. Это работает в однопоточном режиме, но в настоящее время, в эпоху многоядерных, многоядерных, многоуровневых кэшей, оно не будет работать правильно. Прежде всего, он вводит состояние гонки (несколько потоков могут одновременно прочитать значение), но также проблемы с видимостью. Значение может храниться только в « локальной » памяти ЦП (некоторый кэш) и не быть видимым для других ЦП / ядер (и, следовательно, для потоков). Вот почему многие ссылаются на локальную копию переменной в потоке. Это очень небезопасно. Рассмотрим этот популярный, но неработающий код остановки потока:

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

Добавьте volatileк stoppedпеременной, и она будет работать нормально - если какой-либо другой поток изменяет stoppedпеременную через pleaseStop()метод, вы гарантированно увидите это изменение немедленно в while(!stopped)цикле рабочего потока . Кстати, это тоже не хороший способ прервать поток, см .: Как остановить поток, который работает вечно без какого-либо использования, и Остановить определенный поток Java .

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

AtomicIntegerИспользует класс CAS ( сравнения и замены ) низкоуровневых операций ЦП (не требуется никакой синхронизации!) Они позволяют изменять определенную переменную , только если текущее значение равно что - то другое (и возвращается успешно). Поэтому, когда вы выполняете getAndIncrement()его, он фактически запускается в цикле (упрощенная реальная реализация):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

Итак, в основном: читать; попытаться сохранить увеличенное значение; если не удалось (значение больше не равно current), прочитайте и попробуйте снова. compareAndSet()Реализуются в машинном коде (сборка).

volatile без синхронизации

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Этот код не является правильным. Это исправляет проблему видимости ( volatileгарантирует, что другие потоки могут видеть изменения, внесенные в counter), но все еще имеет состояние гонки. Это было объяснено несколько раз: до / после увеличения не является атомарным.

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

volatile без синхронизации (2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

Та же проблема, что и выше, но еще хуже, потому что iнет private. Состояние гонки все еще присутствует. Почему это проблема? Если, скажем, два потока запускают этот код одновременно, вывод может быть + 5или + 10. Тем не менее, вы гарантированно увидите изменения.

Несколько независимых synchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

Сюрприз, этот код также неверен. На самом деле это совершенно неправильно. Во-первых, вы синхронизируете i, что должно быть изменено (более того, iэто примитив, поэтому я предполагаю, что вы синхронизируете временный Integerфайл, созданный через автобокс ...). Вы также можете написать:

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

Никакие два потока не могут войти в один и тот же synchronizedблок с одинаковой блокировкой . В этом случае (и аналогично в вашем коде) объект блокировки изменяется при каждом выполнении, поэтому synchronizedэффективно не действует.

Даже если вы использовали конечную переменную (или this) для синхронизации, код все равно неверен. Две нити могут сначала прочитать , iчтобы tempсинхронно (имеющие одинаковое значение локально в temp), то первое присваивает новое значение i(скажем, от 1 до 6) , а другой делает то же самое (от 1 до 6).

Синхронизация должна охватывать от чтения до присвоения значения. Ваша первая синхронизация не имеет никакого эффекта (чтение intявляется атомарным), а также вторая. На мой взгляд, это правильные формы:

void synchronized incIBy5() {
  i += 5 
}

void incIBy5() {
  synchronized(this) {
    i += 5 
  }
}

void incIBy5() {
  synchronized(this) {
    int temp = i;
    i = temp + 5;
  }
}
Томаш Нуркевич
источник
10
Единственное, что я хотел бы добавить, - это то, что JVM копирует значения переменных в регистры для работы с ними. Это означает, что потоки, работающие на одном процессоре / ядре, могут по-прежнему видеть разные значения для энергонезависимой переменной.
Дэвид Харкнесс
@thomasz: сравнение: AndSet (текущий, текущий + 1) синхронизирован ?? если нет, что происходит, когда два потока выполняют этот метод одновременно ??
Hardik
@Hardik: compareAndSetэто просто тонкая оболочка для операции CAS. Я вхожу в некоторые детали в моем ответе.
Томаш Нуркевич
1
@thomsasz: ОК, я пройти через эту ссылку вопрос и ответил Джон Скит, он говорит , «поток не может читать летучий переменную без проверки , был ли какой - либо другой поток выполнил запись.» но что произойдет, если один поток находится между операцией записи, а второй поток читает ее !! я ошибся ?? Разве это не состояние гонки на атомной операции?
Hardik
3
@Hardik: пожалуйста, создайте еще один вопрос, чтобы получить больше ответов на то, что вы спрашиваете, здесь только вы и я, и комментарии не подходят для того, чтобы задавать вопросы. Не забудьте опубликовать ссылку на новый вопрос здесь, чтобы я мог продолжить.
Томаш Нуркевич
61

Объявление переменной как volatile означает, что изменение ее значения немедленно влияет на фактическое хранение в памяти переменной. Компилятор не может оптимизировать любые ссылки, сделанные на переменную. Это гарантирует, что когда один поток изменяет переменную, все остальные потоки видят новое значение немедленно. (Это не гарантируется для энергонезависимых переменных.)

Объявление атомарной переменной гарантирует, что операции, выполняемые с этой переменной, происходят атомарным образом, то есть все подэтапы операции выполняются в потоке, в котором они выполняются, и не прерываются другими потоками. Например, операция увеличения и проверки требует, чтобы переменная была увеличена, а затем сравнена с другим значением; атомарная операция гарантирует, что оба эти шага будут выполнены, как если бы они были единой неделимой / непрерывной операцией.

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

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

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

Приложение (апрель 2016 г.)

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

Приложение (июль 2016 г.)

Синхронизация происходит на объекте . Это означает, что вызов синхронизированного метода класса заблокирует thisобъект вызова. Статические синхронизированные методы заблокируют сам Classобъект.

Аналогично, для ввода синхронизированного блока требуется блокировка thisобъекта метода.

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

Дэвид Р. Триббл
источник
25

летучий:

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

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

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

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

AtomicXXX:

AtomicXXXклассы поддерживают безблокировочное поточно-ориентированное программирование для отдельных переменных. Эти AtomicXXXклассы (как AtomicInteger) устраняют ошибки несоответствия памяти / побочные эффекты модификации изменчивых переменных, которые были доступны в нескольких потоках.

Когда использовать: Несколько потоков могут читать и изменять данные.

синхронизируются:

synchronizedключевое слово, используемое для защиты метода или блока кода. Создание синхронизированного метода имеет два эффекта:

  1. Во-первых, два вызова одного и synchronizedтого же объекта не могут чередоваться. Когда один поток выполняет synchronizedметод для объекта, все другие потоки, которые вызывают synchronizedметоды для того же блока объекта (приостанавливают выполнение), пока первый поток не завершится с объектом.

  2. Во-вторых, когда synchronizedметод завершается, он автоматически устанавливает отношение «до и после» с любым последующим вызовом synchronizedметода для того же объекта. Это гарантирует, что изменения состояния объекта видны всем потокам.

Когда использовать: Несколько потоков могут читать и изменять данные. Ваша бизнес-логика не только обновляет данные, но и выполняет атомарные операции

AtomicXXXэквивалентно, volatile + synchronizedхотя реализация отличается. AmtomicXXXрасширяет volatileпеременные + compareAndSetметоды, но не использует синхронизацию.

Связанные вопросы SE:

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

Летучий логический против AtomicBoolean

Хорошие статьи для чтения: (выше содержание взято из этих страниц документации)

https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html

Равиндра Бабу
источник
2
Это первый ответ, в котором фактически упоминается семантика описанных ключевых слов / функций, которая происходит до того, как они важны для понимания того, как они на самом деле влияют на выполнение кода. Ответы с более высоким рейтингом пропускают этот аспект.
jhyot
5

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

Два потока не могут войти в синхронизированный блок на одном и том же объекте дважды. Это означает, что два потока могут входить в один и тот же блок на разных объектах. Эта путаница может привести к такому коду.

private Integer i = 0;

synchronized(i) {
   i++;
}

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

Если это правда, чем Как этот atomic.incrementAndGet () работает без синхронизации? и является ли потокобезопасным ??

да. Он не использует блокировку для достижения безопасности потока.

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

И в чем разница между внутренним чтением и записью в Volatile Variable / Atomic Variable ??

Атомный класс использует изменчивые поля. Там нет никакой разницы в области. Разница в выполняемых операциях. Классы Atomic используют операции CompareAndSwap или CAS.

Я прочитал в какой-то статье, что поток имеет локальную копию переменных, что это такое?

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

Это проблема, только когда память распределяется, хотя бы один поток обновляет ее.

Питер Лори
источник
@ Аникет Тхакур, ты уверен в этом? Целое число является неизменным. Поэтому i ++, вероятно, автоматически распакует значение int, увеличит его, а затем создаст новое целое число, которое отличается от предыдущего. Попробуйте сделать i final, и вы получите ошибки компилятора при вызове i ++.
fuemf5
2

Синхронизированный против атомного против летучего:

  • Volatile и Atomic применяются только к переменной, в то время как Synchronized применяются к методу.
  • Изменчивые обеспечивают видимость, а не атомарность / согласованность объекта, в то время как другие обеспечивают видимость и атомарность.
  • Изменчивое хранилище переменных в оперативной памяти, и оно быстрее в доступе, но мы не можем обеспечить безопасность потоков или синхронизацию без синхронизированного ключевого слова.
  • Синхронизированный реализован как синхронизированный блок или синхронизированный метод, в то время как оба не. Мы можем обеспечить многопоточность безопасного кода с помощью синхронизированного ключевого слова, в то время как с обоими мы не можем достичь того же самого.
  • Синхронизированный может заблокировать один и тот же объект класса или другой объект класса, в то время как оба не могут.

Пожалуйста, поправьте меня, если я что-то пропустил.

Аджай Гупта
источник
1

Синхронизация volatile + - это надежное решение для операции (оператора), которая должна быть полностью атомарной, которая включает в себя несколько инструкций для CPU.

Скажем, например: volatile int i = 2; i ++, который является ничем иным, как i = i + 1; что делает i как значение 3 в памяти после выполнения этого оператора. Это включает в себя считывание существующего значения из памяти для i (которое равно 2), загрузку в регистр аккумулятора ЦП и выполнение вычисления путем увеличения существующего значения на единицу (2 + 1 = 3 в аккумуляторе), а затем запись этого увеличенного значения обратно. назад в память. Эти операции не являются достаточно атомарными, хотя значение i является изменчивым. Будучи энергозависимым, я гарантирую только то, что ЕДИНОЕ чтение / запись из памяти является атомарным, а не МНОГОКРАТНЫМ. Следовательно, мы должны синхронизироваться и вокруг i ++, чтобы это было надежным атомарным утверждением. Помните тот факт, что утверждение включает в себя несколько утверждений.

Надеюсь, объяснение достаточно ясно.

Томас Мэтью
источник
1

Модификатор Java volatile является примером специального механизма, гарантирующего взаимодействие между потоками. Когда один поток выполняет запись в энергозависимую переменную, а другой поток видит эту запись, первый поток сообщает второму обо всем содержимом памяти до тех пор, пока не выполнит запись в эту переменную переменную.

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

Саи Пратик
источник