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

чувствительная операция в моей лаборатории сегодня прошла совершенно неправильно. Привод электронного микроскопа вышел за его пределы, и после цепочки событий я потерял 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.

5 ответов


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

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

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

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

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

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


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

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

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

результаты выглядят по-разному, если Вы читаете значения только один раз в потоке для использования в сравнении и последующем отображении их. Когда я делаю следующее x всегда отображается как 1 и y варьируется между 0 и некоторое большое целое число. Я думаю, что поведение этого на данный момент несколько неопределенно без 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);
}

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

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

  • в С. х
  • на стр. г
  • в currentpos

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

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

  1. читает currentpos в p
  2. читать стр. X и С. г (в любом порядке)
  3. сравните и возьмите ветку
  4. читать стр. X и P.г (либо заказать) и системных вызовов.из.метод println

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

  1. он читает currentpos.х сначала х произошла запись-получает 0, затем читает currentpos.y до того, как произошла запись y-получает 0. Сравните evals с true. Пишет стать видимым для системы, т..из.код println называется.
  2. он читает currentpos.x сначала, после того, как произошла запись x, затем читает currentpos.y до того, как произошла запись y-получает 0. Сравните evals с true. Пишет, становятся видимыми, т... так далее.
  3. он читает currentpos.y сначала, до того, как произошла запись y (0), затем считывает currentpos.x после x напишите, эвалс "истина". так далее.

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

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

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

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

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

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

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

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


вы можете использовать объект для синхронизации записи и чтения. В противном случае, как говорили другие ранее, запись в 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);
    }
}

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

например:

  1. x = 10, y = 11
  2. рабочий поток оценивает p.x as 10
  3. основной поток выполняет обновление, теперь x = 11 и y = 12
  4. рабочий поток оценивает p.y as 12
  5. рабочий поток замечает, что 10+1 != 12, так что отпечатки и выходы.

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

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

добавить

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

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