Постфиксный инкремент в Java не является атомарным. Это означает, что в реальности за вызовом i++ скрывается несколько последовательных операций. Переменная сперва считывается из памяти, затем увеличивается на единицу, а результат снова записывается в память.
При использовании операции в многопоточном окружении эта особенность может привести к нежелательным последствиям. Если несколько потоков одновременно выполняют инкремент переменной int i с начальным значением 3, то может случиться так, что поток №1 выполнит чтение в локальную переменную, затем увеличит локальное значение на 1 (iLocal = 4), в этот момент поток №2 считает из памяти iLocal = 3 (ведь инкремент, выполненный потоком №1, еще не был записан в память), поток №1 отправит значение i = 4 в память, а поток №2 увеличит свою локальную переменную на 1 (iLocal = 4) и также запишет полученный результат. В результате оба потока будут конкурировать друг с другом в попытках инкрементировать i. Возникает так называемое состояние гонки.

Попробуем заставить три потока посчитать от 1-го до 30-ти. Рассмотрим класс Main:
public class Main {
// Количество миллисекунд задержки при вызове в цикле
public static final int TIME = 500;
// Количество выполнений цикла в каждом потоке
public static final int QTY = 10;
private long value = 1;
public long getValue() {
return value;
}
// Возвращает инкрементированное значение
public long getNext() {
return value++;
}
}
Здесь long value – инкрементируемое значение, public long getNext() – метод, в котором выполняется инкремент, смысл констант TIME и QTY станет понятен далее.
Создадим класс, наследующий Thread:
class TestThread extends Thread {
Main main;
String threadName;
public TestThread(Main main, String threadName) {
this.main = main;
this.threadName = threadName;
}
@Override
public void run() {
for (int i = 1; i <= Main.QTY; i++) {
System.out.println(i + ". " + threadName + ": " + main.getNext());
try {
Thread.sleep(Main.TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == Main.QTY) {
// Информируем о том, что данная итерация - последняя
System.out.println("Last cycle of: " + threadName + " Target value is: " + main.getValue());
}
}
}
}
В конструктор TestThread передается экземпляр класса Main и имя потока для логирования в консоль. В переопределенном методе run() выполняется цикл от 1 до константы QTY. В каждой итерации цикла вызывается Thread.sleep, который имитирует выполнение неких операций внутри цикла. На последней итерации выводится соответствующее информационное сообщение.
Сведём все воедино в классе Test:
class Test {
public static void main(String[] args) throws InterruptedException {
// Создаём общий экземпляр Main для всех потоков
Main main = new Main();
// Создаём потоки
TestThread th1 = new TestThread(main, "A");
TestThread th2 = new TestThread(main, "B");
TestThread th3 = new TestThread(main, "C");
// Запускаем потоки
th1.start();
th2.start();
th3.start();
}
}
Попробуем несколько раз подряд запустить метод main из класса Test:




Вызов main каждый раз возвращает разные результаты, несмотря на то, что цикл в каждом из потоков выполняется ровно десять раз. Потоки находятся в состоянии гонки, поэтому невозможно с уверенностью сказать, какое значение value будет видно каждому из них в определенный момент времени.
volatile
Проблема примера выше в том, что изменение переменной value не являлось атомарным. Потоки многократно считывали устаревшие значения из памяти и перезаписывали изменения, внесенные другими потоками. Попробуем исправить эту ситуацию, добавив ключевое слово volatile к переменной value (подсказка – это не поможет):
private volatile long value = 1;
Снова запустим метод main:

Результат снова противоречит начальному замыслу. Позволю себе привести вольный перевод определения volatile из статьи Джереми Мэнсона:
Ключевое слово volatile – это пример специального механизма, который гарантирует коммуникацию между потоками. Когда первый поток записывает volatile-переменную и второй поток видит эту запись, первый поток сообщает второму потоку обо всём содержимом памяти до тех пор, пока тот не выполнит запись в эту переменную. Другими словами, изменения volatile-переменной, сделанные в одном потоке, становятся сразу видны всем другим потокам. Нашу проблему невозможно решить простым объявлением переменной value как volatile, так как операция инкремента выполняется в несколько шагов, то в промежуток между чтением value в локальную переменную и сохранением инкрементированного значения обратно в память, могут выполниться несколько итераций аналогичного цикла в другом потоке.
synchronized
Java позволяет избежать состояния гонки путём синхронизации доступа к общим ресурсам. Метод, помеченный как synchronized, может выполняться одновременно только одним потоком.
Изменим метод getNext:
// // Возвращает инкрементированное значение
public synchronized long getNext() {
return value++;
}

Теперь при каждом запуске метод возвращает одинаково корректный результат. Непоследовательность в выводе чисел на консоль может быть обусловлена задержками самой консоли. В каждом блоке итераций мы видим три не повторяющихся числа и в итоге добираемся до 31.
1 Response
[…] поля класса помечены как volatile (см. статью блога. — прим. переводчика), это означает, что они могут […]