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

Как работает атомарная/неустойчивая/синхронизированная работа?

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

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

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

Ответ 1

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

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

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 (compare-and-swap) низкоуровневые операции ЦП (синхронизация не требуется!) Они позволяют изменять конкретная переменная, только если текущее значение равно чем-то другому (и возвращается успешно). Поэтому, когда вы выполняете 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), но все еще имеют условие гонки. Это было объяснено несколько раз: pre/post-incrementation не является атомарным.

Единственный побочный эффект 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, созданном с помощью autoboxing...). Полностью испорчен. Вы также можете написать:

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

Ответ 2

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

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

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

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

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

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

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

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

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

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

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

Ответ 3

летучий:

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

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

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

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

AtomicXXX:

Классы

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

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

синхронизировано:

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

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

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

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

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

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

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

Volatile boolean vs 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

Ответ 4

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

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

private Integer i = 0;

synchronized(i) {
   i++;
}

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

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

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

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

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

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

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

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

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

Ответ 5

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

Скажем, например: volatile int я = 2; я ++, который не что иное, как я = я + 1; что делает я значением 3 в памяти после выполнения этого оператора. Это включает в себя чтение существующего значения из памяти для я (которое равно 2), загрузка в регистр накопителя ЦП и выполнение вычисления путем увеличения существующего значения с помощью одного (2 + 1 = 3 в аккумуляторе), а затем записи назад, обратно в память. Эти операции не являются достаточно атомными, хотя значение я является изменчивым. я volatile гарантирует только то, что SINGLE чтение/запись из памяти является атомарным, а не с MULTIPLE. Следовательно, нам нужно синхронизировать также вокруг я ++, чтобы он был логическим утверждением с дураком. Помните, что оператор содержит несколько операторов.

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

Ответ 6

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

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