V6074. Non-atomic modification of volatile variable.
Анализатор обнаружил неатомарное изменение 'volatile' переменной, которое может привести к состоянию гонки.
Известно, что использование модификатора 'volatile' гарантирует, что все потоки будут видеть актуальное значение соответствующей переменной. К этому можно добавить, что модификатор 'volatile' используется для того, чтобы указать JVM, что все операции присвоения этой переменной и все операции чтения из неё должны быть атомарными.
Можно посчитать, что пометки переменных как 'volatile' будет достаточно, чтобы безопасно их использовать в многопоточном приложении! Но что будет, если изменять 'volatile' переменную, будущее значение которой зависит от текущего?
К таким операциям можно отнести:
- var++, ‑‑var, ...
- var += smt, var *= smt, ...
- ...
Рассмотрим использование 'volatile' переменной в качестве счетчика (counter++).
class Counter
{
private volatile int counter = 0;
....
public void increment()
{
counter++; // counter = counter + 1
}
....
}
Такая операция выглядит как одна операция, но в действительности это целая последовательность операций чтения-изменения-записи. Здесь и кроется состояние гонки.
Предположим, что 2 потока одновременно работают с объектом класса Counter и выполняют инкремент переменной 'counter' (10):
[counter == 10, temp == 10] Поток N1 считывает значение 'counter'во временную переменную.
[counter == 10, temp == 11] Поток N1 изменяет временную переменную.
[counter == 10, temp == 10] Поток N2 считывает значение 'counter'во временную переменную.
[counter == 11, temp == 11] Поток N1 записывает временную переменную в 'counter'.
[counter == 11, temp == 11] Поток N2 изменяет временную переменную.
[counter == 11, temp == 11] Поток N2 записывает временную переменную в 'counter'.
Ожидалось значение переменной 'counter' равное 12 (а не 11), так как 2 потока выполнили инкремент над одной и той же переменной. Также возможна ситуация, когда потоки выполнят инкремент друг за другом, и в таком случае все будет так, как и ожидалось. Как итог, раз на раз не приходится!
Чтобы избежать подобного поведения неатомарных операций для разделяемых переменных, можно использовать:
- Блок 'synchronized',
- Классы из пакета java.util.concurrent.atomic,
- Функциональность блокировок из пакета java.util.concurrent.locks
Пример корректного кода:
class Counter
{
private volatile int counter = 0;
....
public synchronized void increment()
{
counter++;
}
....
}
или
class Counter
{
private final AtomicInteger counter = new AtomicInteger(0);
....
public void increment()
{
counter.incrementAndGet();
}
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6074. |