Ви конкретно запитуєте, як вони працюють всередині , тож ось ви:
Немає синхронізації
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)
циклі робочої нитки . BTW - це не гарний спосіб переривання потоку, див. Як зупинити потік, який працює вічно, без будь-якого використання та Зупинення конкретного потоку 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;
}
}