Прежде всего, произвольный порядок выполнения потока не является самой гонкой данных, даже если это может вызвать ее.Если вам нужно синхронизировать 2 или более потоков для выполнения их кода в определенном порядке, вы должны использовать механизм ожидания, такой как мониторы .Мониторы - это конструкции, которые могут выполнять как взаимное исключение (блокировка) , так и ожидание.Мониторы также известны как условные переменные, и Java поддерживает их.
Теперь вопрос в том, что такое гонка данных.Гонка данных происходит, когда 2 или более потоков обращаются к одной и той же ячейке памяти в одно и то же время, и некоторые из обращений являются записями.Эта ситуация может привести к непредсказуемым значениям, которые может содержать ячейка памяти.
Классический пример.Позволяет иметь 32-битную ОС и переменную длиной 64 бита, как у типов long
или double
.Давайте иметь переменную long
.
long SharedVariable;
И поток 1, который выполняет следующий код.
SharedVariable=0;
И поток 2, который выполняет следующий код.
SharedVariable=0x7FFF_FFFF_FFFF_FFFFL;
Если доступ к этой переменной не защищен блокировкой, после выполнения обоих потоков SharedVariable
может иметь одно из следующих значений.
SharedVariable==0
SharedVariable==0x7FFF_FFFF_FFFF_FFFFL
**SharedVariable==0x0000_0000_FFFF_FFFFL**
**SharedVariable==0x7FFF_FFFF_0000_0000L**
Последние 2 значения являются неожиданными -вызванный гонкой данных.
Проблема здесь в том, что в 32-битных ОС существует гарантия того, что доступ к 32-битным переменным является атомарным - поэтому платформа гарантирует, что даже если 2 или более потоков обращаются кта же 32-битная ячейка памяти, в то же время доступ к этой ячейке памяти является атомарным - только одна нить может получить доступ к такой переменной.Но поскольку у нас есть 64-битная переменная, на уровне ЦП переменная записи в 64-битную длину преобразуется в 2 инструкции ЦП.Таким образом, код SharedVariable=0;
переводится примерно так:
mov SharedVariableHigh32bits,0
mov SharedVariableLow32bits,0
А код SharedVariable=0x7FFF_FFFF_FFFF_FFFFL;
переводится примерно так:
mov SharedVariableHigh32bits,0x7FFFFFFF
mov SharedVariableLow32bits,0xFFFFFFFF
Без блокировки процессор можетвыполните эти 4 инструкции в следующих приказах.
Заказ 1.
mov SharedVariableHigh32bits,0 // T1
mov SharedVariableLow32bits,0 // T1
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableLow32bits,0xFFFFFFFF // T2
Результат: 0x7FFF_FFFF_FFFF_FFFFL
.
Заказ 2.
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableLow32bits,0xFFFFFFFF // T2
mov SharedVariableHigh32bits,0 // T1
mov SharedVariableLow32bits,0 // T1
Результат: 0
.
Заказ 3.
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableHigh32bits,0 // T1
mov SharedVariableLow32bits,0 // T1
mov SharedVariableLow32bits,0xFFFFFFFF // T2
Результат: 0x0000_0000_FFFF_FFFFL
.
Заказ 4.
mov SharedVariableHigh32bits,0 // T1
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableLow32bits,0xFFFFFFFF // T2
mov SharedVariableLow32bits,0 // T1
Результат: 0x7FFF_FFFF_0000_0000L
.
Итак, условие гонки вызвало серьезную проблему, поскольку вы можете получить значение, которое является совершенно неожиданным и недействительным.Используя блокировки, вы можете предотвратить это, но простое использование блокировки не гарантирует порядок выполнения - какой поток выполняет свой код первым.Поэтому, если вы используете блокировку, вы получите только 2 порядка исполнения - порядок 1 и порядок 2, а не неожиданные значения 0x0000_0000_FFFF_FFFFL
и 0x7FFF_FFFF_0000_0000L
.Но, тем не менее, если вам нужно синхронизировать, какой поток выполняется первым, а какой - вторым, вам нужно использовать не только блокировку, но и механизм ожидания, который предлагает мониторинг (условные переменные).
BTW в соответствии с этим статья , Java гарантирует атомарный доступ ко всем переменным примитивного типа, кроме long
и double
.На 64-битных платформах даже доступ к long
и double
должен быть атомарным, но похоже, что стандарт не гарантирует этого.
И даже если стандарт гарантирует атомарный доступ, он всегда будетлучше использовать замки.Блокировки определяют барьеры памяти , которые препятствуют некоторым оптимизациям компилятора, которые могут переупорядочить ваш код на уровне команд ЦП и вызывают проблемы при использовании переменных для управления порядком выполнения.
Итак, простоесовет здесь заключается в том, что если вы не являетесь экспертом по параллельному программированию (а я тоже нет) и не пишете SW, который должен получить абсолютную максимальную производительность с помощью методов без блокировок, всегда используйте блокировки - даже если обращаетесь к переменнымкоторые гарантированно имеют атомарный доступ.