Тестирование безопасности инициализации конечных полей - PullRequest
28 голосов
/ 21 февраля 2011

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

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

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();
        }
    }
}

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

Ответы [ 8 ]

18 голосов
/ 20 марта 2013

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

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

6 голосов
/ 21 февраля 2011

Начиная с 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);
4 голосов
/ 14 марта 2013

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

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

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

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

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

Как определить, не поврежден ли многопоточный код?

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

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

На практике некоторые JVM лучше справятся с неудачным тестом.Например, некоторые компиляторы (см. «Тестовый пример, показывающий, что он не работает» в этой статье ) могут преобразовать 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) - это было бы фактически законным исполнением, если бы поток никогда не наблюдал эту операцию записи.Поэтому при правильном чередовании потоков возможно, что если reader работает между 4 и 5, вы получите желаемый результат.

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

2 голосов
/ 26 апреля 2013

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

0 голосов
/ 17 марта 2013

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

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

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 экземпляр.
0 голосов
/ 14 марта 2013

Как вы изменили конструктор, чтобы сделать это:

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

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

0 голосов
/ 12 марта 2013

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

public class TestClass {

    final int x;
    static TestClass f;

    public TestClass() {
        x = 3;
    }

    int y = 4;

    // etc...

}

0 голосов
/ 11 марта 2013

Лучшее понимание того, почему этот тест не проходит, может быть получено из понимания того, что на самом деле происходит при вызове конструктора.Java - это основанный на стеке язык.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.exe -XX: + UnlockDiagnosticVMOptions -XX: + PrintAssembly -Xcomp -XX: PrintAssemblyOptions = hsdis-print-bytes -XX: CompileCommand = print, * Test.main Test

Вывод:

...
0x0263885d: movl   $0x20,0x8(%eax)    ;...c7400820 000000
                                    ;*putfield myVariable1
                                    ; - Test::<init>@7 (line 12)
                                    ; - Test::main@4 (line 17)
0x02638864: movl   $0x40,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 не отличается от любого другого потока.Если такие гарантии предоставляются GC, то они предоставляются любому другому потоку.Это наиболее очевидное решение для такого ограничения.

Результаты:

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

2) Присвоение ссылки на объект не может быть выполнено до возврата конструктора.

...