Почему volatile в 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.

я иногда получаю "ошибка" напечатано. Как такое возможно?
4 66

4 ответа:

обновление:

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

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

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

как только нить видит a == 1 тогда этот поток гарантированно увидит b == 1.

начиная с Java 5, в ФП, например, после того, как while(a == 0) вспыхивает b гарантированно будет 1

Edit:

я запустил моделирование много раз и не сделал смотрите свой вывод.

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

я на Windows 7, Java 1.6_24 (пытаюсь с _31)

Edit 2:

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

Edit 3:

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

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

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

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

 0x021dd753: test   %eax,0x180100      ;   {poll}
  0x021dd759: cmp    x0,%ecx
  0x021dd75c: je     0x021dd748         ;*ifeq
                                        ; - Test::run@7 (line 13)
  0x021dd75e: cmp    x0,%edx
  0x021dd761: jne    0x021dd788         ;*ifne
                                        ; - Test::run@13 (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::run@16 (line 18)
  0x021dd776: cmp    (%ecx),%eax        ; implicit exception: dispatches to 0x021dd7dc
  0x021dd778: mov    x39239500,%edx   ;*invokevirtual println

и

это не напечатать "ошибка"

0x0226d763: test   %eax,0x180100      ;   {poll}
  0x0226d769: cmp    x0,%edx
  0x0226d76c: je     0x0226d758         ;*ifeq
                                        ; - Test::run@7 (line 13)
  0x0226d76e: mov    x341b77f8,%edx   ;   {oop('Test')}
  0x0226d773: mov    0x154(%edx),%edx   ;*getstatic b
                                        ; - Test::access@0 (line 3)
                                        ; - Test::run@10 (line 17)
  0x0226d779: cmp    x0,%edx
  0x0226d77c: jne    0x0226d7a8         ;*ifne
                                        ; - Test::run@13 (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::run@16 (line 18)
  0x0226d796: cmp    (%ecx),%eax        ; implicit exception: dispatches to 0x0226d811
  0x0226d798: mov    x39239500,%edx   ;*invokevirtual println

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

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

  0x0226d76e: mov    x341b77f8,%edx   ;   {oop('Test')}
  0x0226d773: mov    0x154(%edx),%edx   ;*getstatic b
                                        ; - Test::access@0 (line 3)
                                        ; - Test::run@10 (line 17)
  0x0226d779: cmp    x0,%edx
  0x0226d77c: jne    0x0226d7a8         ;*ifne
                                        ; - Test::run@13 (line 17)

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

  0x021dd75e: cmp    x0,%edx
  0x021dd761: jne    0x021dd788         ;*ifne
                                        ; - Test::run@13 (line 17)

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

Edit 4

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

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

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

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

похоже, что проблема более легко воспроизводится с клиентом JVM (- client).

на мой взгляд,проблема обострилась из-за отсутствия синхронизация:

обратите внимание : Если b=1 heppens перед a=1, а a является изменчивым, А b нет, то b=1 фактически обновляется для всех потоков только после завершения a=1 (в соответствии с логикой quate).

то, что heppend в вашем коде является то, что b=1 был сначала обновлен только для основного процесса, а затем только после завершения изменчивого назначения, все потоки B обновлены. Я думаю, что может быть назначения volatile не работают как атомарные операции (нужно указывать далеко и каким-то образом обновлять остальные ссылки, чтобы действовать как летучие), поэтому я бы предположил, почему один поток читает 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;
        }  
    }
}