Тестирование безопасности инициализации конечных полей

Я пытаюсь просто проверить безопасность инициализации конечных полей, гарантированную JLS. Это для статьи, которую я пишу. Однако я не могу заставить его "сбой" на основе моего текущего кода. Может ли кто-нибудь сказать мне, что я делаю неправильно, или это просто то, что я должен пробежать снова и снова, а затем увидеть неудачу с каким-то неудачным временем?

вот мой код:

public class TestClass {

    final int x;
    int y;
    static TestClass f;

    public TestClass() {
        x = 3;
        y = 4;
    }

    static void writer() {
        TestClass.f = new TestClass();
    }

    static void reader() {
        if (TestClass.f != null) {
            int i = TestClass.f.x; // guaranteed to see 3
            int j = TestClass.f.y; // could see 0

            System.out.println("i = " + i);
            System.out.println("j = " + j);
        }
    }
}

и мои потоки называют это так:

public class TestClient {

    public static void main(String[] args) {

        for (int i = 0; i < 10000; i++) {
            Thread writer = new Thread(new Runnable() {
                @Override
                public void run() {
                    TestClass.writer();
                }
            });

            writer.start();
        }

        for (int i = 0; i < 10000; i++) {
            Thread reader = new Thread(new Runnable() {
                @Override
                public void run() {
                    TestClass.reader();
                }
            });

            reader.start();
        }
    }
}

I запустите этот сценарий много раз. Мои текущие циклы порождают 10 000 потоков, но я сделал с этим 1000, 100000 и даже миллион. До сих пор ни одного сбоя. Я всегда вижу 3 и 4 для обоих значений. Как я могу это провалить?

8 ответов


из Java 5.0 вы уверены, что все потоки увидят конечное состояние, установленное конструктором.

Если вы хотите увидеть этот сбой, вы можете попробовать более старый JVM, такой как 1.3.

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

более простой способ увидеть этот сбой-добавить к писателю.

f.y = 5;

и тест для

int y = TestClass.f.y; // could see 0, 4 or 5
if (y != 5)
    System.out.println("y = " + y);

Я написал спец. Версия TL; DR этого ответа-это просто потому, что она мая см. 0 для y, это не означает, что это гарантированно см. 0 для y.

в этом случае окончательная спецификация поля гарантирует, что вы увидите 3 для x, как вы указываете. Подумайте о потоке writer как имеющем 4 Инструкции:

r1 = <create a new TestClass instance>
r1.x = 3;
r1.y = 4;
f = r1;

причина, по которой вы не можете увидеть 3 для x, заключается в том, что компилятор переупорядочил этот код:

r1 = <create a new TestClass instance>
f = r1;
r1.x = 3;
r1.y = 4;

путь гарантия для конечных полей обычно реализуется на практике, чтобы убедиться, что конструктор заканчивается до любых последующих действий программы. Представьте, что кто-то установил большой барьер между r1.y = 4 и f = r1. Так, на практике, если у вас есть какие-либо окончательные поля для объекта, вы, вероятно, получите видимость для всех из них.

теперь, теоретически, кто-то может написать компилятор, который не реализован таким образом. В самом деле, многие люди часто говорили о тестировании кода написание самого вредоносного компилятора. Это особенно распространено среди людей C++, у которых есть много и много неопределенных углов их языка, которые могут привести к ужасным ошибкам.


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

многопоточность и тестирования

вы не можете доказать, что многопоточное приложение будет работать (или) тестирование по нескольким причинам:

  • проблема может появляться только один раз в x часов работы, x настолько высок, что вряд ли вы увидите его в коротком тесте
  • проблема может только появляются с некоторыми комбинациями архитектур JVM / processor

в вашем случае, чтобы сделать тестовый перерыв (т. е. наблюдать y == 0), потребуется, чтобы программа увидела частично построенный объект, где некоторые поля были правильно построены, а некоторые нет. Обычно это не происходит на x86 / hotspot.

как определить, если многопоточный код нарушен?

единственный способ доказать, что код действителен или сломан, - применить правила JLS к нему и посмотреть, что результат. При публикации гонки данных (без синхронизации вокруг публикации объекта или y) JLS не гарантирует, что y будет рассматриваться как 4 (его можно увидеть со значением по умолчанию 0).

может ли этот код действительно сломаться?

на практике некоторые JVMs будут лучше делать тест неудачным. Например, некоторые компиляторы (cf "тестовый случай, показывающий, что он не работает" в в этой статье) может трансформироваться TestClass.f = new TestClass(); во что-то вроде (потому что он публикуется через гонку данных):

(1) allocate memory
(2) write fields default values (x = 0; y = 0) //always first
(3) write final fields final values (x = 3)    //must happen before publication
(4) publish object                             //TestClass.f = new TestClass();
(5) write non final fields (y = 4)             //has been reodered after (4)

JLS предписывает, что (2) и (3) происходят до публикации объекта (4). Однако из - за гонки данных нет гарантии для (5) - это фактически было бы законным исполнением, если поток никогда не наблюдал эту операцию записи. При правильном чередовании нитей поэтому возможно, что if reader работает между 4 и 5, вы получите желаемый результат.

I у вас нет symantec JIT под рукой, поэтому не могу доказать это экспериментально : -)


здесь является примером значений по умолчанию не конечных значений, наблюдаемых, несмотря на то, что конструктор устанавливает их и не протекает this. Это основано на моем другой вопрос что немного сложнее. Я продолжаю видеть, как люди говорят, что это не может произойти на x86, но мой пример происходит на x64 linux openjdk 6...


что о тебе изменен конструктор для этого:

public TestClass() {
 Thread.sleep(300);
   x = 3;
   y = 4;
}

Я не эксперт по финалам и инициализаторам JLF, но здравый смысл говорит мне, что это должно задержать настройку x достаточно долго, чтобы авторы зарегистрировали другое значение?


Что делать, если изменить сценарий на

public class TestClass {

    final int x;
    static TestClass f;

    public TestClass() {
        x = 3;
    }

    int y = 4;

    // etc...

}

?


лучшее понимание того, почему этот тест не терпит неудачу, может исходить из понимания того, что на самом деле происходит при вызове конструктора. Ява-стековый язык. TestClass.f = new TestClass(); состоит из четырех действий. Первый new вызывается инструкция, ее как malloc в C / C++, она выделяет память и помещает ссылку на нее в верхней части стека. Затем ссылка дублируется для вызова конструктора. Конструктор фактически похож на любой другой метод экземпляра, его вызывается с дублированным ссылка. Только после этого ссылка сохраняется в фрейме метода или в поле экземпляра и становится доступной из любого другого места. Перед последним шагом ссылка на объект присутствует только в верхней части стека создания потока, и никакое тело больше не может его видеть. На самом деле нет никакой разницы, с каким полем вы работаете, оба будут инициализированы, если TestClass.f != null. Вы можете читать поля x и y из разных объектов, но это не приведет к y = 0. Для получения дополнительной информации вы должны увидеть спецификация JVM и стек-ориентированный язык программирования статьи.

UPD: одну важную вещь я забыл упомянуть. По памяти java нет способа увидеть частично инициализированный объект. Если вы не делаете самостоятельные публикации внутри конструктора, конечно.

JLS:

объект считается полностью инициализирован, когда его заканчивает конструктор. Нить, которая может видеть только ссылка на объект после того, как этот объект был полностью инициализирован, гарантируется чтобы увидеть правильно инициализированные значения для конечного объекта поля.

JLS:

существует происходит-перед ребром из конца конструктора объект для запуска финализатора для этого объекта.

более широкое объяснение этой точки зрения:

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

UPD: это была теория, перейдем к практике.

рассмотрим следующий код, с простым не-окончательным переменные:

public class Test {

    int myVariable1;
    int myVariable2;

    Test() {
        myVariable1 = 32;
        myVariable2 = 64;
    }

    public static void main(String args[]) throws Exception {
        Test t = new Test();
        System.out.println(t.myVariable1 + t.myVariable2);
    }
}

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

java.exe-XX: + UnlockDiagnosticVMOptions-XX: + PrintAssembly-Xcomp - XX: PrintAssemblyOptions=hsdis-печать-байты-XX: CompileCommand=печать, * тест.главный тест

это выход:

...
0x0263885d: movl   x20,0x8(%eax)    ;...c7400820 000000
                                    ;*putfield myVariable1
                                    ; - Test::<init>@7 (line 12)
                                    ; - Test::main@4 (line 17)
0x02638864: movl   x40,0xc(%eax)    ;...c7400c40 000000
                                    ;*putfield myVariable2
                                    ; - Test::<init>@13 (line 13)
                                    ; - Test::main@4 (line 17)
0x0263886b: nopl   0x0(%eax,%eax,1)   ;...0f1f4400 00
...

за полевыми заданиями следуют NOPL инструкция, одна из ее целей является запретить переупорядочивание инструкций.

почему это происходит? согласно спецификации завершение происходит после возвращения конструктора. Таким образом, поток GC не может видеть частично инициализированный объект. На уровне процессора поток GC не отличается от любого другого потока. Если такие гарантии предоставляются ГК, то они предоставляются любому другому потоку. Это наиболее очевидное решение проблемы ограничение.

результаты:

1) конструктор не синхронизирован, синхронизация выполняется другие инструкции.

2) назначение ссылки на объект не может произойти до возвращения конструктора.


что происходит в этой теме? Почему это код не на первом месте?

вы запускаете 1000 потоков, каждый из которых будет делать следующее:

TestClass.f = new TestClass();

что это значит, по порядку:

  1. оценить TestClass.f чтобы узнать его местоположение памяти
  2. оценить new TestClass(): это создает новый экземпляр TestClass, конструктор которого инициализирует оба x и y
  3. присвойте правой значение местоположение памяти левой руки

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

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

TestClass.f всегда будет содержать:
  • или null в начале вашей программы, прежде чем что-либо еще, возложенные на него,
  • или полностью инициализирован TestClass экземпляра.