Давайте сначала разберемся с Data Race и Race Condition по отдельности.

The Java Language Specification говорит нам что когда программа содержит два конфликтующих доступа(например read и write), которые не упорядочены отношением "happens-before", говорят, что она содержит Data Race.

Довольно размытое определение, давайте лучше посмотрим что такое Data Race на практике.

int x // общий для всех потоков ресурс

thread1 {
  x = 1 // параллельно изменяет общий ресурс
}

thread2 {
  x = 2 // параллельно изменяет общий ресурс
}

thread3 {
  read x // параллельно читает общий ресурс
}

В этом случае мы можем прочитать старое значение из нашего поля х, это называется чтение через гонку.

В этом примере у нас есть Data Race, но нет Race Condition, чтобы избавиться от Data Race нам нужно упорядочить наши операции чтения и помощью отношения "happens-before".

Мы можем сделать это с помощью добавления ключевого слова "volatile", Это ключевое слово имеет семантику для видимости памяти. По сути, значение изменчивого поля становится видимым для всех считывателей (в частности, для других потоков) после завершения операции записи в него. Без volatile читатели могли бы увидеть некоторое не обновленное значение.

volatile int x // общий для всех потоков ресурс

thread1 {
  x = 1 // параллельно изменяет общий ресурс
}

thread2 {
  x = 2 // параллельно изменяет общий ресурс
}

thread3 {
  read x // параллельно читает общий ресурс
}

В этом случае наша программа не содержит Data Race и также не содержит Race Condition, а это значит, что наша программа (согласно Java Language Specification) "корректно синхронизованна".

Теперь поговорим о Race Condition

Java Concurrency на практике говорит нам, что "при Race Condition возникает возможность появления неправильных результатов из-за неудачной временной координации потоков".

Понятнее не стало... Ладно, посмотрим на практике.

volatile int x // общий для всех потоков ресурс

incrementx() {
  x++
}
getx() {
  return x
}

thread1 {
  incrementx() // параллельно инкрементирует общий ресурс
}
thread2 {
  incrementx() // параллельно инкрементирует общий ресурс
}
thread3 {
  incrementx() // параллельно инкрементирует общий ресурс
}

thread4 {
  getx() // параллельно читает общий ресурс
}

На этом примере наша программа не содержит Data Race, но содержит Race Condition, второй поток может "переписать" значение, записанное первым потоком. Для того чтобы решить эту проблему обычного volatile недостаточно, тут нам поможет синхронизация (или Atomic типы данных). Решим мы эту проблему с помощью клучевого слова synchronized.

volatile int x // общий для всех потоков ресурс

synchronized incrementx() {
  x++
}
getx() {
  return x
}

thread1 {
  incrementx() // параллельно инкрементирует общий ресурс
}
thread2 {
  incrementx() // параллельно инкрементирует общий ресурс
}
thread3 {
  incrementx() // параллельно инкрементирует общий ресурс
}

thread4 {
  getx() // параллельно читает общий ресурс
}

Кстати мы можно не делать synchronized метод getx(), так как поле х у нас volatile.

Сейчас рассмотрим вариант в котором наша программа имеет как Data Race, так и Race Condition.

int x // общий для всех потоков ресурс

incrementx() {
  x++
}
getx() {
  return x
}

thread1 {
  incrementx() // параллельно инкрементирует общий ресурс
}
thread2 {
  incrementx() // параллельно инкрементирует общий ресурс
}
thread3 {
  incrementx() // параллельно инкрементирует общий ресурс
}

thread4 {
  getx() // параллельно читает общий ресурс
}

Прочитать эта программа может все что угодно.

Под конец могу сказать, что гораздо полезнее выявлять Race Condition, чем Data Race, Data Race даже иногда может быть полезным и приводить к увеличению производительности (реализованно в String.class в Java).

Комментарии (7)


  1. m1llark Автор
    12.09.2023 14:17

    Пишите коментарии или дополнения!


  1. lao2r
    12.09.2023 14:17

    Возможно я что-то упускаю, но не могу понять, как в Вашем примере проверялась работа volatile, у меня всё равно проскакивает то тут, то там значение 0. Мой код ниже:

    Hidden text
    public class Example {
        volatile int x; // общий для всех потоков ресурс
    
        public void exec() {
    
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    x = 1; // параллельно изменяет общий ресурс
                }
            });
    
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    x = 2; // параллельно изменяет общий ресурс
                }
            });
    
            Thread thread3 = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(x); // параллельно читает общий ресурс
                }
            });
    
            thread1.start();
            thread2.start();
            thread3.start();
        }
    }
    
    
    public class Main {
    
        public static void main(String[] args) throws NoSuchMethodException {
    
            while(true) {
                Example example = new Example();
                example.exec();
            }
    
        }
    
    }


    1. m1llark Автор
      12.09.2023 14:17
      +1

      Добрый день, компилятор в реализации виртуальной машины Java может изменить порядок кода или процессора. Кроме того, иерархия памяти архитектуры, на которой выполняется реализация виртуальной машины Java, может создавать впечатление, что код переупорядочивается.

      В вашем случае скорее всего проблема в том, что JMV делает reordering и thread3.start() выполняется первым.


      1. ris58h
        12.09.2023 14:17
        +1

        Да даже без reordering нет никаких гарантий в каком порядке будут выполняться треды.


    1. anton_t
      12.09.2023 14:17

      -


    1. anton_t
      12.09.2023 14:17
      +2

      Ну так она и должна выводить либо 0, либо 1, либо 2. Все правильно.


  1. Deosis
    12.09.2023 14:17
    +3

    Data Race проще продемонстрировать на двух переменных:

    int x = 0;
    int y = 0;
    thread1 {
      x = 1;
      y = 1;
    }
    thread2 {
      if (y > 0) {
        print(x);
      }
    }

    В этом варианте нельзя утверждать, что второй поток выведет единицу, так как процессор может переставить местами операции записи в память.