Testowanie bezpieczeństwa inicjalizacji końcowych pól

Próbuję po prostu przetestować bezpieczeństwo inicjalizacji końcowych pól gwarantowane przez JLS. To do pracy, którą piszę. Jednak nie jestem w stanie zmusić go do "porażki" w oparciu o mój obecny kod. Czy ktoś może mi powiedzieć, co robię źle, lub czy to jest coś, co muszę przejechać w kółko i zobaczyć porażkę z jakimś pechowym wyczuciem czasu?

Oto Mój kod:

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

A moje wątki nazywają to tak:

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

Przeprowadziłem ten scenariusz wiele, wiele razy. Moje obecne pętle rodzą 10 000 wątków, ale zrobiłem z tym 1000, 100000, a nawet milion. Nadal bez porażki. Zawsze widzę 3 i 4 dla obu wartości. Jak Mogę sprawić, że to się nie uda?

Author: palacsint, 2011-02-21

8 answers

Od wersji Java 5.0 masz gwarancję, że wszystkie wątki będą widzieć ostateczny stan ustawiony przez konstruktor.

Jeśli chcesz zobaczyć tę porażkę, możesz spróbować starszego JVM, takiego jak 1.3.

Nie wydrukowałbym każdego testu, tylko błędy. Możesz dostać jedną porażkę na milion, ale ją przegapisz. Ale jeśli drukujesz tylko błędy, powinny być łatwe do wykrycia.

Prostszym sposobem, aby zobaczyć tę porażkę, jest dodanie do pisarza.

f.y = 5;

I test na

int y = TestClass.f.y; // could see 0, 4 or 5
if (y != 5)
    System.out.println("y = " + y);
 6
Author: Peter Lawrey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-21 14:33:59

Napisałem spec. Wersja TL; DR tej odpowiedzi jest taka, że tylko dlatego, że Może zobaczyć 0 dla y, nie oznacza to, że gwarantuje zobaczyć 0 dla y.

W tym przypadku, ostateczna Specyfikacja pola gwarantuje, że zobaczysz 3 dla x, jak wskazujesz. Myśl o wątku pisarskim jako o 4 instrukcjach:

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

Powodem, dla którego możesz nie zobaczyć 3 dla x, jest to, że kompilator zmienił kolejność kodu:

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

Sposób, w jaki gwarancja na pola końcowe jest zwykle zaimplementowane w praktyce jest upewnienie się, że konstruktor zakończy działanie przed dokonaniem kolejnych działań programu. Wyobraź sobie, że ktoś zbudował wielką barierę między r1.y = 4 i f = r1. Tak więc w praktyce, jeśli masz jakieś ostateczne pola dla obiektu, prawdopodobnie uzyskasz widoczność dla wszystkich z nich.

Teraz, teoretycznie, ktoś mógłby napisać kompilator, który nie jest zaimplementowany w ten sposób. W rzeczywistości wiele osób często mówiło o testowaniu kodu, pisząc najbardziej złośliwy kompilator. Jest to szczególnie powszechne wśród ludzi C++, którzy mają wiele, wiele nieokreślonych zakątków swojego języka, które mogą prowadzić do strasznych błędów.

 16
Author: Jeremy Manson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-20 06:55:26

Chciałbym zobaczyć test, który się nie powiedzie lub wyjaśnienie, dlaczego nie jest to możliwe przy obecnych JVMs.

Wielowątkowość i testowanie

Nie można udowodnić, że aplikacja wielowątkowa jest zepsuta (lub nie) przez testowanie z kilku powodów:

  • problem może pojawić się tylko raz na X godziny pracy, x jest tak wysoki, że jest mało prawdopodobne, że zobaczysz go w krótkim teście
  • problem może pojawić się tylko w niektórych kombinacjach JVM / architektury procesorów

W Twoim przypadku, wykonanie przerwy testowej (tzn. obserwacja y == 0) wymagałoby, aby program zobaczył częściowo skonstruowany obiekt, w którym niektóre pola zostały prawidłowo skonstruowane, a inne nie. Zazwyczaj nie dzieje się to na x86 / hotspot.

Jak ustalić, czy wielowątkowy kod jest uszkodzony?

Jedynym sposobem, aby udowodnić, że kod jest ważny lub złamany, jest zastosowanie do niego zasad JLS i sprawdzenie, jaki jest wynik. Z data race publikowanie (brak synchronizacji wokół publikacji obiektu lub Y), JLS nie daje gwarancji, że y będzie postrzegane jako 4 (może być postrzegane z domyślną wartością 0).

Czy ten kod naprawdę może się złamać?

W praktyce, niektóre JVM będą lepsze w sprawieniu, że test się nie powiedzie. Na przykład niektóre Kompilatory (por. "przypadek testowy pokazujący, że nie działa" w ten artykuł ) mogą przekształcić TestClass.f = new TestClass(); w coś podobnego (ponieważ jest on publikowany za pomocą danych wyścig): {]}

(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 nakazuje, aby (2) i (3) miały miejsce przed publikacją obiektu (4). Jednak ze względu na wyścig danych, nie udziela się gwarancji dla (5) - byłoby to w rzeczywistości legalne wykonanie, gdyby wątek nigdy nie zaobserwował tej operacji zapisu. Przy odpowiednim przeplataniu wątków jest zatem możliwe, że jeśli reader działa między 4 a 5, otrzymasz pożądane wyjście.

Nie mam Symantec JIT pod ręką więc nie mogę tego udowodnić eksperymentalnie: -)

 4
Author: assylias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-14 11:43:18

Tutaj {[3] } jest przykład domyślnych wartości nie końcowych, które są obserwowane pomimo tego, że konstruktor je ustawia i nie wycieka this. Jest to oparte na moim innym pytaniu , które jest nieco bardziej skomplikowane. Ciągle widzę, że ludzie mówią, że to nie może się zdarzyć na x86, ale mój przykład dzieje się na x64 linux openjdk 6...

 2
Author: Dog,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:09:45

A co z modyfikacją konstruktora, aby to zrobić:

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

Nie jestem ekspertem od JLF finals i initializers, ale zdrowy rozsądek mówi mi, że powinno to opóźnić ustawienie x wystarczająco długo, aby pisarze zarejestrowali inną wartość?

 -1
Author: Jakub Zaverka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-14 07:52:16

Co jeśli zmienimy scenariusz na

public class TestClass {

    final int x;
    static TestClass f;

    public TestClass() {
        x = 3;
    }

    int y = 4;

    // etc...

}

?

 -2
Author: Alexander Vasiljev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-12 06:22:19

Lepsze zrozumienie, dlaczego ten test nie zawodzi, może wynikać ze zrozumienia tego, co faktycznie się dzieje, gdy wywoływany jest konstruktor. Java jest językiem opartym na stosie. TestClass.f = new TestClass(); składa się z czterech akcji. Pierwsza instrukcja new jest wywoływana, podobnie jak malloc w C / C++, przydziela pamięć i umieszcza odniesienie do niej na górze stosu. Następnie Referencja jest duplikowana dla wywołania konstruktora. Constructor w rzeczywistości jest jak każda inna metoda instancji, jej wywołana z duplikowanym odniesieniem. Tylko następnie Referencja jest przechowywana w ramce metody lub w polu wystąpienia i staje się dostępna z dowolnego miejsca. Przed ostatnim krokiem odniesienie do obiektu jest obecne tylko na górze stosu wątku i żaden inny obiekt nie może go zobaczyć. W rzeczywistości nie ma różnicy, z jakim polem pracujesz, oba zostaną zainicjalizowane, Jeśli TestClass.f != null. Można odczytywać pola x i y z różnych obiektów, ale nie spowoduje to y = 0. Więcej informacji można znaleźć w JVM Specification and Stack-oriented programming language articles.

UPD: jedna ważna rzecz, o której zapomniałem wspomnieć. Przez pamięć java nie można zobaczyć częściowo zainicjowanego obiektu. Jeśli nie robisz własnych publikacji wewnątrz konstruktora, oczywiście.

JLS :

Obiekt jest uważany za całkowicie zainicjalizowany, gdy jego konstruktor kończy. Wątek, który widzi tylko odniesienie do obiekt po tym obiekcie został całkowicie zainicjowany jest gwarantowany aby zobaczyć poprawnie zainicjowane wartości dla finalnego obiektu pola.

JLS :

Istnieje krawędź happens-before od końca konstruktora obiekt do początku finalizatora dla tego obiektu.

Szersze wyjaśnienie tego punktu widzenia :

Okazuje się, że koniec konstruktora obiektu następuje-przed wykonanie jego metody finalize. W praktyka, co to oznacza jest że wszelkie zapisy występujące w konstruktorze muszą być zakończone i widoczne dla dowolnego odczytu tej samej zmiennej w finalizatorze, tak jak gdyby te zmienne były zmienne.

UPD: taka była teoria, przejdźmy do praktyki.

Rozważ następujący kod, z prostymi zmiennymi niekończącymi się:

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

Poniższe polecenie wyświetla instrukcje maszynowe wygenerowane przez Javę, jak z niej korzystać można znaleźć w wiki :

Java.exe-XX: + UnlockDiagnosticVMOptions-XX: + PrintAssembly-Xcomp -XX: PrintAssemblyOptions=hsdis-print-bytes-XX: CompileCommand=print, * Test.Test Główny

Jest wyjście:

...
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
...

Po przydzieleniu pola następuje instrukcjanopl , jednym z jej celów jestzapobieganie zmianie kolejności instrukcji .

Dlaczego tak się dzieje? zgodnie ze specyfikacją finalizacja następuje po powrocie konstruktora. Więc Wątek GC nie może zobaczyć częściowo zainicjowanego obiektu. Na poziomie CPU wątek GC nie jest odróżniany od żadnego innego wątku. Jeśli takie gwarancje są dostarczane do GC, niż są one dostarczane do jakiegokolwiek innego wątku. Jest to najbardziej oczywiste rozwiązanie takiego ograniczenia.

Wyniki:

1) Konstruktor nie jest zsynchronizowany, synchronizacja odbywa się za pomocą innych instrukcji .

2) przypisanie do przechyłki odniesienia obiektu następuje przed konstruktorem zwroty.

 -2
Author: Mikhail,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-12 07:31:22

Co się dzieje w tym wątku? Dlaczego ten kod miałby się nie udać?

Uruchamiasz 1000 wątków, które wykonają następujące czynności:

TestClass.f = new TestClass();

Co to robi, w kolejności:

  1. Oceń TestClass.f, aby dowiedzieć się, gdzie znajduje się jego pamięć
  2. evaluate new TestClass(): tworzy to nową instancję TestClass, której konstruktor zainicjalizuje zarówno x, jak i y
  3. przypisanie wartości prawej do pamięci lewej lokalizacja

Przypisanie jest operacją atomową, która jest zawsze wykonywana po wygenerowaniu wartości prawej strony . oto cytat ze specyfikacji języka Java (zobacz pierwszy punkt punktowy), ale tak naprawdę odnosi się do każdego zdrowego języka.

Oznacza to, że podczas gdy konstruktor TestClass() poświęca swój czas, aby wykonać swoje zadanie, a x i y mogą być nadal zerowe, odniesienie do częściowo zainicjowanego TestClass obiektu żyje tylko w procesor ten nie został zapisany do TestClass.f

Dlatego TestClass.f zawsze będzie zawierać:

  • albo null, na początku programu, zanim cokolwiek zostanie do niego przypisane,
  • lub w pełni zainicjalizowaną instancją TestClass.
 -3
Author: Tobia,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-17 11:14:55