Вы конкретно спрашиваете, как они работают внутри , так что вот вы:
Нет синхронизации
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;
}
}