В чем разница между атомарным / энергозависимым / синхронизированным?
Как атомарный / энергозависимый / синхронизированный работает внутри?
В чем разница между следующими блоками кода?
Код 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 ответов
Вы конкретно спрашиваете о том, как они работают внутри, так что вот вы:
Нет синхронизации
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;
}
}
Объявление переменной как volatile означает, что изменение ее значения немедленно влияет на фактическое хранение в памяти для переменной. Компилятор не может оптимизировать любые ссылки, сделанные на переменную. Это гарантирует, что когда один поток изменяет переменную, все остальные потоки видят новое значение немедленно. (Это не гарантируется для энергонезависимых переменных.)
Объявление атомарной переменной гарантирует, что операции, выполняемые с этой переменной, происходят атомарным образом, то есть все подэтапы операции выполняются в потоке, в котором они выполняются, и не прерываются другими потоками. Например, операция приращения и проверки требует, чтобы переменная увеличивалась, а затем сравнивалась с другим значением; атомарная операция гарантирует, что оба эти шага будут выполнены, как если бы они были единой неделимой / бесперебойной операцией.
Синхронизация всех обращений к переменной позволяет только одному потоку одновременно обращаться к переменной и заставляет все другие потоки ждать, пока этот доступный поток освободит свой доступ к переменной.
Синхронизированный доступ аналогичен атомарному доступу, но атомарные операции обычно реализуются на более низком уровне программирования. Кроме того, вполне возможно синхронизировать только некоторые обращения к переменной и разрешить несинхронизировать другие обращения (например, синхронизировать все записи в переменную, но ни одно из чтений из нее).
Атомарность, синхронизация и изменчивость являются независимыми атрибутами, но обычно используются в комбинации для обеспечения правильного взаимодействия потоков для доступа к переменным.
Приложение (апрель 2016 г.)
Синхронизированный доступ к переменной обычно осуществляется с помощью монитора или семафора. Это низкоуровневые механизмы взаимного исключения (взаимоисключения), которые позволяют потоку получать контроль исключительно над переменной или блоком кода, заставляя все другие потоки ждать, если они также попытаются получить тот же мьютекс. Как только поток-владелец освобождает мьютекс, другой поток может получить его по очереди.
Приложение (июль 2016 г.)
Синхронизация происходит на объекте. Это означает, что вызов синхронизированного метода класса заблокирует this
объект вызова. Статические синхронизированные методы заблокируют Class
сам объект
Аналогично, вход в синхронизированный блок требует блокировки this
Объект метода.
Это означает, что синхронизированный метод (или блок) может выполняться в нескольких потоках одновременно, если они блокируются на разных объектах, но только один поток может одновременно выполнять синхронизированный метод (или блок) для любого данного отдельного объекта.
летучий:
volatile
это ключевое слово. volatile
вынуждает все потоки получать последнее значение переменной из основной памяти вместо кеша. Для доступа к переменным переменным блокировка не требуется. Все потоки могут получить доступ к значению переменной переменной одновременно.
С помощью volatile
Переменные уменьшают риск ошибок согласованности памяти, потому что любая запись в энергозависимую переменную устанавливает связь "до того, как произойдет", с последующим чтением этой же переменной.
Это означает, что меняется на volatile
переменные всегда видны другим потокам. Более того, это также означает, что когда поток читает volatile
переменная, она видит не только последние изменения в volatile, но и побочные эффекты кода, который привел к изменению.
Когда использовать: один поток изменяет данные, а другие потоки должны считывать последние значения данных. Другие потоки предпримут какое-то действие, но не обновят данные.
AtomicXXX:
AtomicXXX
классы поддерживают поточнобезопасное программирование для отдельных переменных. Эти AtomicXXX
классы (как AtomicInteger
) устраняет ошибки несоответствия памяти / побочные эффекты модификации изменчивых переменных, которые были доступны в нескольких потоках.
Когда использовать: несколько потоков могут читать и изменять данные.
синхронизируются:
synchronized
это ключевое слово, используемое для защиты метода или блока кода. Создание синхронизированного метода имеет два эффекта:
Во-первых, это невозможно для двух вызовов
synchronized
методы одного и того же объекта для чередования. Когда один поток выполняетsynchronized
метод для объекта, все остальные потоки, которые вызываютsynchronized
методы для того же блока объекта (приостановить выполнение), пока первый поток не будет завершен с объектом.Во-вторых, когда
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
Я знаю, что два потока не могут войти в блок синхронизации одновременно
Два потока не могут войти в синхронизированный блок на одном и том же объекте дважды. Это означает, что два потока могут входить в один и тот же блок для разных объектов. Эта путаница может привести к такому коду.
private Integer i = 0;
synchronized(i) {
i++;
}
Это не будет вести себя так, как ожидалось, поскольку каждый раз может блокироваться на другом объекте.
Если это правда, чем Как этот atomic.incrementAndGet() работает без синхронизации?? и является ли потокобезопасным??
да. Он не использует блокировку для достижения безопасности потока.
Если вы хотите узнать, как они работают более подробно, вы можете прочитать код для них.
И в чем разница между внутренним чтением и записью в Volatile Variable / Atomic Variable??
Атомный класс использует изменчивые поля. Там нет никакой разницы в области. Разница заключается в выполненных операциях. Классы Atomic используют операции CompareAndSwap или CAS.
Я прочитал в какой-то статье, что поток имеет локальную копию переменных, что это такое?
Я могу только предположить, что это относится к тому факту, что каждый процессор имеет свое собственное кэшированное представление памяти, которое может отличаться от любого другого процессора. Чтобы ваш ЦП имел согласованное представление данных, вам необходимо использовать методы обеспечения безопасности потоков.
Это только проблема, когда память разделяется, хотя бы один поток обновляет ее.
Синхронизированный против атомного против летучего:
1. Volatile и Atomic применяются только к переменной, в то время как Synchronized применяются к методу.
2. Летучие обеспечивают видимость, а не атомарность / согласованность объекта, в то время как другие обеспечивают видимость и атомарность.
3. Хранилище изменчивых переменных в оперативной памяти, и оно быстрее в доступе, но мы не можем обеспечить безопасность потоков или синхронизацию без синхронизированного ключевого слова.
4. Синхронизированный реализован как синхронизированный блок или синхронизированный метод, в то время как оба не Мы можем обеспечить многопоточность безопасного кода с помощью синхронизированного ключевого слова, в то время как с обоими мы не можем достичь того же самого.
5. Синхронизированный может заблокировать один и тот же объект класса или другой объект класса, в то время как оба не могут.
Пожалуйста, поправьте меня, если я что-то пропустил.
Синхронизация volatile + - это надежное решение для операции (оператора), которая должна быть полностью атомарной, которая включает в себя несколько инструкций для CPU.
Скажем, например:volatile int i = 2; i++, который является ничем иным, как i = i + 1; что делает i как значение 3 в памяти после выполнения этого оператора. Это включает в себя считывание существующего значения из памяти для i(который равен 2), загрузку в регистр аккумулятора ЦП и выполнение вычисления путем увеличения существующего значения на единицу (2 + 1 = 3 в аккумуляторе), а затем запись этого увеличенного значения назад в память. Эти операции не являются достаточно атомарными, хотя значение i является изменчивым. Будучи энергозависимым, я гарантирую только то, что ЕДИНОЕ чтение / запись из памяти является атомарным, а не МНОГОКРАТНЫМ. Следовательно, мы должны синхронизироваться и вокруг i ++, чтобы это было надежным атомарным утверждением. Помните тот факт, что утверждение включает в себя несколько утверждений.
Надеюсь, объяснение достаточно ясно.
Модификатор Java volatile является примером специального механизма, гарантирующего взаимодействие между потоками. Когда один поток записывает в переменную переменной, а другой поток видит эту запись, первый поток сообщает второму обо всем содержимом памяти до тех пор, пока не выполнит запись в эту переменную переменную.
Атомарные операции выполняются в одной задаче без вмешательства других операций. Атомарные операции необходимы в многопоточной среде, чтобы избежать несогласованности данных.