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