В чем разница между атомарным / энергозависимым / синхронизированным? - PullRequest
271 голосов
/ 17 марта 2012

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

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

Код 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? И это потокобезопасно?

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

Ответы [ 7 ]

357 голосов
/ 17 марта 2012

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

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

private int counter;

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

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

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));

Итак, в основном: read;попытаться сохранить увеличенное значение;если не удалось (значение больше не равно 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;
  }
}
52 голосов
/ 27 января 2015

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

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

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

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

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

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

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

Добавление (июль 2016 г.)

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

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

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

17 голосов
/ 23 мая 2016

volatile:

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

5 голосов
/ 17 марта 2012

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

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

private Integer i = 0;

synchronized(i) {
   i++;
}

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

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

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

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

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

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

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

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

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

2 голосов
/ 01 ноября 2018

Синхронизировано против атомного против изменчивого:
1. Volatile и Atomic применяются только к переменной, в то время как Synchronized применяются к методу.
2. Летучие обеспечивают видимость, а не атомарность / согласованность объекта, в то время как другие обеспечивают видимость и атомарность.
3. Изменчивое хранилище переменных в оперативной памяти и более быстрый доступ, но мы не можем достичь безопасности потоков или синхронизации без синхронизированного ключевого слова.
4. Синхронизированный реализован как синхронизированный блок или синхронизированный метод, в то время как оба нет. Мы можем обеспечить многопоточность безопасного кода с помощью синхронизированного ключевого слова, в то время как с обоими мы не можем достичь того же самого.
5. Синхронизированный может заблокировать один и тот же объект класса или другой объект класса, в то время как оба не могут.
Пожалуйста, поправьте меня, если я что-то пропустил.

1 голос
/ 04 июля 2016

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

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

1 голос
/ 26 августа 2015

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

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

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

...