Pull to refresh

Разница между Data Race и Race Condition

Level of difficultyEasy
Reading time3 min
Views9.5K

Давайте сначала разберемся с 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).

Tags:
Hubs:
Total votes 3: ↑2 and ↓1+3
Comments8

Articles