Почему не изменчивость в java 5+ обеспечивает видимость из другого потока?

В соответствии с:

http://www.ibm.com/developerworks/library/j-jtp03304/

В новой модели памяти, когда поток A записывает в изменчивую переменную V, а поток B считывает из V, любые значения переменных, которые были видны A в момент написания V, теперь гарантированы, чтобы быть видимыми для B

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

public class Test {
    volatile static private int a;
    static private int b;

    public static void main(String [] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread() {

                @Override
                public void run() {
                    int tt = b; // makes the jvm cache the value of b

                    while (a==0) {

                    }

                    if (b == 0) {
                        System.out.println("error");
                    }
                }

            }.start();
        }

        b = 1;
        a = 1;
    }
}

b должен быть 1 для всех потоков, когда a равно 1.

Однако я иногда получаю сообщение об ошибке. Как это возможно?

Ответ 1

Update:

Для всех, кого эта ошибка была устранена и исправлена ​​для Java 7u6 build b14. Здесь вы можете увидеть отчет об ошибках/исправления.

Оригинальный ответ

Когда вы думаете о видимости/порядке памяти, вам нужно подумать о ее происхождении - перед отношениями. Важным предварительным условием для b != 0 является a == 1. Если a != 1, то b может быть либо 0, либо 1.

Как только поток видит a == 1, тогда этот поток, как ожидается, увидит b == 1.

Опубликовать Java 5, в примере OP, как только while(a == 0) breaks out b будет 1

Edit:

Я запускал симуляцию много раз и не видел вашего вывода.

Какие ОС, версия Java и процессор вы тестируете?

Я нахожусь в Windows 7, Java 1.6_24 (пытается с _31)

Изменить 2:

Престижность OP и Walter Laan - для меня это произошло только тогда, когда я переключился с 64-разрядной Java на 32-битную Java, на (но не может быть исключен) на 64-битные окна 7.

Изменить 3:

Назначение tt, или, скорее, статический элемент b, как представляется, имеет существенное влияние (чтобы доказать это, удалите int tt = b; и он всегда должен работать.

Похоже, что загрузка b в tt будет сохранять поле локально, которое затем будет использоваться в конидированной системе if (ссылка на это значение не tt). Поэтому, если b == 0 истинно, это, вероятно, означает, что локальный магазин до tt равен 0 (в этот момент его гонка назначит 1 локальному tt). Это, похоже, справедливо только для 32-битных Java 1.6 и 7 с набором клиентов.

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

Эта печатная "ошибка"

 0x021dd753: test   %eax,0x180100      ;   {poll}
  0x021dd759: cmp    $0x0,%ecx
  0x021dd75c: je     0x021dd748         ;*ifeq
                                        ; - Test$1::[email protected] (line 13)
  0x021dd75e: cmp    $0x0,%edx
  0x021dd761: jne    0x021dd788         ;*ifne
                                        ; - Test$1::[email protected] (line 17)
  0x021dd767: nop    
  0x021dd768: jmp    0x021dd7b8         ;   {no_reloc}
  0x021dd76d: xchg   %ax,%ax
  0x021dd770: jmp    0x021dd7d2         ; implicit exception: dispatches to 0x021dd7c2
  0x021dd775: nop                       ;*getstatic out
                                        ; - Test$1::[email protected] (line 18)
  0x021dd776: cmp    (%ecx),%eax        ; implicit exception: dispatches to 0x021dd7dc
  0x021dd778: mov    $0x39239500,%edx   ;*invokevirtual println

И

Это не печатало "error"

0x0226d763: test   %eax,0x180100      ;   {poll}
  0x0226d769: cmp    $0x0,%edx
  0x0226d76c: je     0x0226d758         ;*ifeq
                                        ; - Test$1::[email protected] (line 13)
  0x0226d76e: mov    $0x341b77f8,%edx   ;   {oop('Test')}
  0x0226d773: mov    0x154(%edx),%edx   ;*getstatic b
                                        ; - Test::[email protected] (line 3)
                                        ; - Test$1::[email protected] (line 17)
  0x0226d779: cmp    $0x0,%edx
  0x0226d77c: jne    0x0226d7a8         ;*ifne
                                        ; - Test$1::[email protected] (line 17)
  0x0226d782: nopw   0x0(%eax,%eax,1)
  0x0226d788: jmp    0x0226d7ed         ;   {no_reloc}
  0x0226d78d: xchg   %ax,%ax
  0x0226d790: jmp    0x0226d807         ; implicit exception: dispatches to 0x0226d7f7
  0x0226d795: nop                       ;*getstatic out
                                        ; - Test$1::[email protected] (line 18)
  0x0226d796: cmp    (%ecx),%eax        ; implicit exception: dispatches to 0x0226d811
  0x0226d798: mov    $0x39239500,%edx   ;*invokevirtual println

В этом примере первая запись - это пробег, в котором напечатана "ошибка", а вторая - от той, которая не сделала.

Кажется, что рабочий запуск загружен и назначен b правильно, прежде чем тестировать его равным 0.

  0x0226d76e: mov    $0x341b77f8,%edx   ;   {oop('Test')}
  0x0226d773: mov    0x154(%edx),%edx   ;*getstatic b
                                        ; - Test::[email protected] (line 3)
                                        ; - Test$1::[email protected] (line 17)
  0x0226d779: cmp    $0x0,%edx
  0x0226d77c: jne    0x0226d7a8         ;*ifne
                                        ; - Test$1::[email protected] (line 17)

В то время как запуск, который печатал "ошибку", загружал кешированную версию %edx

  0x021dd75e: cmp    $0x0,%edx
  0x021dd761: jne    0x021dd788         ;*ifne
                                        ; - Test$1::[email protected] (line 17)

Для тех, у кого больше опыта работы с ассемблером, пожалуйста, взвешивайте:)

Изменить 4

Должно быть мое последнее редактирование, так как concurrency dev получить руку на нем, я проверил с и без int tt = b; присвоить еще несколько. Я обнаружил, что когда я увеличиваю максимальный размер от 100 до 1000, кажется, что 100% -ная ошибка при включении int tt = b и 0% вероятности, когда он исключен.

Ответ 2

Основываясь на выдержке из JCiP ниже, я бы подумал, что ваш пример никогда не должен печатать "ошибка":

Эффекты видимости изменчивых переменных распространяются вне значения самой изменчивой переменной. Когда поток A записывает в изменчивую переменную, а затем поток B считывает эту же переменную, значения всех переменных, которые были видимы A перед записью в изменчивую переменную, становятся видимыми B после прочтения изменчивой переменной.

Ответ 3

Возможно, вы захотите проверить тему обсуждения в списке рассылки concurrency по этому вопросу: http://cs.oswego.edu/pipermail/concurrency-interest/2012-May/009440.html

Кажется, что проблему легче воспроизвести с помощью JVM клиента (-клиента).

Ответ 4

По моему мнению, проблема возникла из-за отсутствия синхронизации:

ВНИМАНИЕ:, если b = 1 heppens перед a = 1, а a является volatile, а b - нет, тогда b = 1 фактически обновляет все потоки только после завершения a = 1 (согласно логика quate).

что heppend в вашем коде состоит в том, что b = 1 был сначала обновлен только для основного процесса, тогда только когда завершено волатильное присвоение, все потоки b обновлены. Я думаю, что, возможно, назначения volatile не работают как атомарные операции (нужно указывать далеко и как-то обновлять остальные ссылки, чтобы действовать как volatiles), так что это было бы моим предположением, почему один поток читает b = 0 вместо b = 1.

Рассмотрим это изменение кода, которое показывает мое утверждение:

public class Test {
    volatile static private int a;
    static private int b;
    private static Object lock = new Object();


    public static void main(String [] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread() {

                @Override
                public void run() {
                    int tt = b; // makes the jvm cache the value of b

                    while (true) {
                        synchronized (lock ) {
                            if (a!=0) break;
                         }
                    }

                    if (b == 0) {
                        System.out.println("error");
                    }
                }

            }.start();
        }
        b = 1;
        synchronized (lock ) {
        a = 1;
        }  
    }
}