Чувствительная операция в моей лаборатории сегодня прошла совсем не так. Привод на электронном микроскопе перешел ее границы, и после череды событий я потерял оборудование на 12 миллионов долларов. Я сузил более 40K строк в неисправном модуле так:
import java.util.*;
class A {
static Point currentPos = new Point(1,2);
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
new Thread() {
void f(Point p) {
synchronized(this) {}
if (p.x+1 != p.y) {
System.out.println(p.x+" "+p.y);
System.exit(1);
}
}
@Override
public void run() {
while (currentPos == null);
while (true)
f(currentPos);
}
}.start();
while (true)
currentPos = new Point(currentPos.x+1, currentPos.y+1);
}
}
Некоторые примеры вывода я получаю:
$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651
Поскольку здесь нет никакой арифметики с плавающей запятой, и мы все знаем, что целые числа со знаком хорошо работают при переполнении в Java, я думаю, что в этом коде нет ничего плохого. Однако, несмотря на вывод, указывающий, что программа не достигла условия выхода, она достигла условия выхода (она была достигнута и не достигнута?). Зачем?
Я заметил, что это не происходит в некоторых средах. Я на OpenJDK 6 на 64-битной Linux.
final
квалификатора (который не влияет на производимый байт-код) к полямx
иy
«решает» ошибку. Хотя это не влияет на байт-код, поля помечаются им, что заставляет меня думать, что это побочный эффект оптимизации JVM.Point
p
которое удовлетворяетp.x+1 == p.y
, затем ссылка передается потоку опроса. В конце концов, поток опроса решает выйти, потому что считает, что условие не выполняется для одного изPoint
s, которые он получает, но затем вывод консоли показывает, что он должен быть выполнен. Отсутствиеvolatile
здесь просто означает, что поток опроса может застрять, но это явно не проблема.synchronized
делает ошибку не происходит? Это потому, что мне приходилось писать код случайным образом, пока я не нашел код, который воспроизводил бы это поведение детерминистически.Ответы:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
делает несколько вещей, в том числе записывает значения по умолчанию вx
andy
(0) и затем записывает их начальные значения в конструктор. Поскольку ваш объект не публикуется безопасно, эти 4 операции записи могут быть свободно переупорядочены компилятором / JVM.Таким образом, с точки зрения потока чтения, это законное выполнение для чтения
x
с новым значением, ноy
со значением по умолчанию, например, 0. К тому времени, когда вы достигнетеprintln
оператора (который, кстати, синхронизирован и, следовательно, влияет на операции чтения), переменные имеют свои начальные значения, и программа выводит ожидаемые значения.Пометка
currentPos
asvolatile
обеспечит безопасную публикацию, поскольку ваш объект является практически неизменным - если в вашем реальном случае объект мутирует после создания,volatile
гарантий не будет достаточно, и вы снова увидите несогласованный объект.В качестве альтернативы вы можете сделать
Point
неизменяемым, что также обеспечит безопасную публикацию даже без использованияvolatile
. Чтобы добиться неизменности, вам просто нужно пометитьx
иy
окончательно.В качестве дополнительного примечания и, как уже упоминалось,
synchronized(this) {}
JVM может рассматривать его как запрет на выполнение операций (насколько я понимаю, вы включили его для воспроизведения поведения).источник
Поскольку
currentPos
изменяется вне потока, он должен быть помечен какvolatile
:Без volatile поток не может считывать обновления в currentPos, которые производятся в основном потоке. Таким образом, новые значения продолжают записываться для currentPos, но поток продолжает использовать предыдущие кэшированные версии из соображений производительности. Поскольку только один поток модифицирует currentPos, вы можете обойтись без блокировок, которые улучшат производительность.
Результаты будут сильно отличаться, если вы прочитаете значения только один раз в потоке для использования в сравнении и последующем их отображении. Когда я делаю следующее
x
всегда отображается как1
иy
меняется между0
и некоторым большим целым числом. Я думаю, что его поведение в данный момент несколько неопределенно безvolatile
ключевого слова, и вполне возможно, что JIT-компиляция кода способствует тому, чтобы он действовал так. Также, если я закомментирую пустойsynchronized(this) {}
блок, тогда код также работает, и я подозреваю, что это происходит потому, что блокировка вызывает достаточную задержку,currentPos
и ее поля перечитываются, а не используются из кэша.источник
volatile
.У вас есть обычная память, ссылка «currentpos» и объект Point и его поля позади него, совместно используемые двумя потоками без синхронизации. Таким образом, не существует определенного порядка между записями, которые происходят с этой памятью в главном потоке, и чтениями в созданном потоке (назовите его T).
Основной поток выполняет следующие записи (игнорирование начальной установки точки приведет к тому, что px и py будут иметь значения по умолчанию):
Поскольку в этих записях нет ничего особенного с точки зрения синхронизации / барьеров, среда выполнения свободна, чтобы позволить потоку T видеть, что они происходят в любом порядке (основной поток, конечно, всегда видит записи и чтения, упорядоченные в соответствии с порядком программы), и происходит в любой момент между чтениями в Т.
Итак, T делает:
Учитывая отсутствие порядка упорядочения между записями в main и чтениями в T, есть несколько способов, которыми это может привести к вашему результату, так как T может увидеть запись main в currentpos перед записью в currentpos.y или currentpos.x:
и так далее ... Здесь есть несколько гонок данных.
Я подозреваю, что ошибочное предположение состоит в том, что мы думаем, что записи, полученные в результате этой строки, становятся видимыми во всех потоках в программном порядке потока, выполняющего его:
Java не дает такой гарантии (это было бы ужасно для производительности). Необходимо добавить еще кое-что, если вашей программе требуется гарантированный порядок записей относительно операций чтения в других потоках. Другие предложили сделать поля x, y окончательными или, в качестве альтернативы, сделать текущие переменными.
Преимущество использования final в том, что оно делает поля неизменяемыми и, таким образом, позволяет кэшировать значения. Использование volatile приводит к синхронизации при каждой записи и чтении currentpos, что может снизить производительность.
См. Главу 17 Спецификации языка Java для подробностей: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html.
(Первоначальный ответ предполагал более слабую модель памяти, так как я не был уверен, что JLS гарантировала, что энергозависимость была достаточной. Ответ отредактирован, чтобы отразить комментарий от ассилий, указав, что модель Java более сильная - происходит до того, как транзитивна - и такой изменчивости на currentpos также достаточно ).
источник
currentPos
задание является изменчивым, назначение обеспечивает безопасную публикациюcurrentPos
объекта, а также его элементов, даже если они сами не являются изменчивыми.Point currentPos = new Point(x, y)
вас есть 3 записи: (w1)this.x = x
, (w2)this.y = y
и (w3)currentPos = the new point
. Порядок программы гарантирует, что hb (w1, w3) и hb (w2, w3). Позже в программе вы прочитали (r1)currentPos
. ЕслиcurrentPos
не является изменчивым, то между r1 и w1, w2, w3 нет hb, поэтому r1 может наблюдать любой (или ни один) из них. С volatile вы вводите hb (w3, r1). И отношение hb является транзитивным, поэтому вы также вводите hb (w1, r1) и hb (w2, r1). Это кратко изложено в разделе «Параллелизм Java на практике» (3.5.3. Идиомы безопасной публикации).Вы можете использовать объект для синхронизации записи и чтения. В противном случае, как уже говорили другие, запись в currentPos произойдет в середине двух операций чтения p.x + 1 и py
источник
sem
оно не является общим, и обработать синхронизированный оператор как недопустимый ... Тот факт, что он решает проблему, является чистой удачей.Вы получаете доступ к currentPos дважды и не гарантируете, что он не обновляется между этими двумя доступами.
Например:
По сути, вы сравниваете две разные точки.
Заметьте, что даже создание текущей переменной volPos не защитит вас от этого, так как это два отдельных чтения рабочим потоком.
Добавить
метод к вашему классу очков. Это обеспечит использование только одного значения currentPos при проверке x + 1 == y.
источник