>
>
>
V6074. Non-atomic modification of volat…


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.