В чем разница между atomic / volatile / synchronized?


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

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

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

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

6   231   2012-03-17 15:46:41

6 ответов:

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

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

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

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

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

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

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

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

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

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

дополнительное соглашение(апрель 2016 года)

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

дополнительное соглашение(июль 2016)

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

volatile:

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

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

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

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

AtomicXXX:

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

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

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

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

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

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

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

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

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

разница между volatile и synchronized в 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

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

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

private Integer i = 0;

synchronized(i) {
   i++;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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