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