Почему эта программа Java заканчивается, несмотря на то, что, по-видимому, она не должна (а не)?

Чувствительная операция в моей лаборатории сегодня прошла совершенно неправильно. Привод на электронном микроскопе прошел через его границу, и после цепочки событий я потерял 12 миллионов долларов оборудования. Я сузил более 40 тыс. Строк в неисправном модуле:

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.

Ответ 1

Очевидно, что запись в currentPos не происходит - перед ее чтением, но я не вижу, как это может быть проблемой.

currentPos = new Point(currentPos.x+1, currentPos.y+1); выполняет несколько действий, включая запись значений по умолчанию x и y (0), а затем запись их начальных значений в конструкторе. Поскольку ваш объект не опубликован безопасно, эти 4 операции записи могут быть свободно переупорядочены компилятором /JVM.

Итак, с точки зрения потока чтения, это законное исполнение для чтения x с его новым значением, но y с его значением по умолчанию 0. К тому времени, когда вы достигнете инструкции println (которая, кстати, синхронизирована и, следовательно, влияет на операции чтения), переменные имеют свои начальные значения, а программа печатает ожидаемые значения.

Маркировка currentPos как volatile обеспечит безопасную публикацию, так как ваш объект является фактически неизменным - если в вашем реальном прецеденте объект мутирован после построения, гарантии volatile не будут достаточными, и вы можете увидеть несогласованность объект снова.

В качестве альтернативы вы можете сделать Point неизменяемым, что также обеспечит безопасную публикацию, даже без использования volatile. Чтобы достичь неизменности, вам просто нужно отметить x и y final.

В качестве побочного примечания и, как уже упоминалось, synchronized(this) {} можно рассматривать как нет-op JVM (я понимаю, вы включили его для воспроизведения поведения).

Ответ 2

Так как currentPos изменяется вне потока, он должен быть помечен как volatile:

static volatile Point currentPos = new Point(1,2);

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

Результаты выглядят значительно иначе, если вы читаете значения только один раз в потоке для использования при сравнении и последующем их отображении. Когда я делаю следующее, x всегда отображается как 1 и y изменяется между 0 и некоторым большим целым числом. Я думаю, что поведение этого в данный момент несколько undefined без ключевого слова volatile, и возможно, что компиляция кода JIT кода вносит свой вклад в это, действуя следующим образом. Также, если я прокомментирую пустой блок synchronized(this) {}, тогда код работает так же, и я подозреваю, что это происходит потому, что блокировка вызывает достаточную задержку, которая currentPos и ее поля перечитываются, а не используются из кеша.

int x = p.x + 1;
int y = p.y;

if (x != y) {
    System.out.println(x+" "+y);
    System.exit(1);
}

Ответ 3

У вас есть обычная память, ссылка "currentpos" и объект Point и его поля позади нее, разделенные между двумя потоками без синхронизации. Таким образом, не существует определенного порядка между сообщениями, которые происходят с этой памятью в основном потоке, и чтениями в созданном потоке (назовите его T).

Основной поток выполняет следующие записи (игнорируя начальную настройку точки, приведет к тому, что p.x и p.y будут иметь значения по умолчанию):

  • to p.x
  • to p.y
  • to currentpos

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

Итак, T делает:

  • читает currentpos для p
  • читайте p.x и p.y(в любом порядке)
  • сравните и возьмите ветку
  • прочитайте p.x и p.y(любой порядок) и вызовите System.out.println

Учитывая отсутствие отношений упорядочения между основными в записи и чтениями в T, существует несколько способов, которыми это может привести к вашему результату, поскольку T может видеть основную запись в currentpos перед записью в currentpos.y или currentpos.x

  • Сначала он считывает currentpos.x, прежде чем произойдет запись x - получает 0, затем читает currentpos.y до того, как возникла запись y - получает 0. Сравните evals с true. Запись становится видимым для T. System.out.println.
  • Он сначала считывает currentpos.x, после того, как возникла запись x, затем читает currentpos.y до того, как возникла запись y - получает 0. Сравните evals с true. Записи становятся видимыми для T... и т.д.
  • Он читает currentpos.y во-первых, до того, как произошла запись y (0), затем читает currentpos.x после записи x, evals в true. и др.

и т.д. Здесь есть несколько гонок данных.

Я подозреваю, что ошибочное предположение здесь предполагает, что записи, полученные из этой строки, становятся видимыми во всех потоках в программном порядке выполняемого им потока:

currentPos = new Point(currentPos.x+1, currentPos.y+1);

Java не дает такой гарантии (это было бы ужасно для производительности). Что-то еще нужно добавить, если ваша программа нуждается в гарантированном порядке записи относительно чтения в других потоках. Другие предложили сделать окончательные поля x, y или, альтернативно, сделать volatile volatile.

  • Если вы сделаете окончательные поля x, y, то Java гарантирует, что записи их значений будут видны до появления конструктора во всех потоках. Таким образом, поскольку назначение currentpos после конструктора, T-поток гарантированно увидит записи в правильном порядке.
  • Если вы делаете currentpos volatile, тогда Java гарантирует, что это точка синхронизации, которая будет полностью упорядочена по другим точкам синхронизации. Как и в основном, записи в x и y должны происходить до записи в currentpos, тогда любое чтение currentpos в другом потоке должно также видеть записи x, y, которые были раньше.

Использование final имеет то преимущество, что оно делает поля неизменяемыми и, таким образом, позволяет кэшировать значения. Использование volatile приводит к синхронизации при каждой записи и чтении currentpos, что может повредить производительность.

См. главу 17 Спецификации языка Java для деталей gora: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

(Первоначальный ответ предполагал более слабую модель памяти, так как я не был уверен, что гарантированная летучесть JLS была достаточной. Ответ отредактирован, чтобы отразить комментарий от assylias, указывая на то, что модель Java сильнее - бывает - раньше транзитивна и поэтому неустойчива на currentpos также хватает).

Ответ 4

Вы можете использовать объект для синхронизации записей и чтения. В противном случае, как говорили другие ранее, запись в currentPos будет происходить в середине двух прочтений p.x + 1 и p.y.

new Thread() {
    void f(Point p) {
        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();
Object sem = new Object();
while (true) {
    synchronized(sem) {
        currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}

Ответ 5

Вы дважды обращаетесь к currentPos и ​​не гарантируете, что он не обновляется между этими двумя обращениями.

Например:

  • x = 10, y = 11
  • рабочий поток оценивает p.x как 10
  • основной поток выполняет обновление, теперь x = 11 и y = 12
  • рабочий поток оценивает p.y как 12
  • рабочий поток замечает, что 10 + 1!= 12, поэтому печатает и завершает работу.

Вы по существу сравниваете две разные точки.

Обратите внимание, что даже включение currentPos volatile не защитит вас от этого, так как это два отдельных чтения рабочего потока.

Добавьте

boolean IsValid() { return x+1 == y; }

для вашего класса очков. Это гарантирует, что при проверке x + 1 == y используется только одно значение currentPos.