Dlaczego ten program Java się kończy mimo, że najwyraźniej nie powinien (i nie powinien)?

Delikatna operacja w moim laboratorium poszła nie tak. Siłownik na mikroskopie elektronowym przekroczył swoją granicę i po całym łańcuchu zdarzeń straciłem sprzęt wart 12 milionów dolarów. Zawęziłem ponad 40K linii w wadliwym module do tego:

import java.util.*;

class A {
    static Point currentPos = new Point(1,2);
    static class Point {
        int x;
        int y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    public static void main(String[] args) {
        new Thread() {
            void f(Point p) {
                synchronized(this) {}
                if (p.x+1 != p.y) {
                    System.out.println(p.x+" "+p.y);
                    System.exit(1);
                }
            }
            @Override
            public void run() {
                while (currentPos == null);
                while (true)
                    f(currentPos);
            }
        }.start();
        while (true)
            currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}

Niektóre próbki wyjścia, które otrzymuję:

$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651

Ponieważ nie ma tu arytmetyki zmiennoprzecinkowej, a wszyscy wiemy, że liczby całkowite podpisane zachowują się dobrze na przepełnieniu w Javie, myślę, że nie ma nic źle z tym kodem. Jednakże, pomimo wyjścia wskazującego, że program nie osiągnął stanu wyjścia, osiągnął stan wyjścia (zarówno osiągnięty , jak i nie został osiągnięty?). Dlaczego?


Zauważyłem, że to się nie zdarza w niektórych środowiskach. Jestem na OpenJDK 6 na 64-bitowym Linuksie.

Author: Machavity, 2013-04-23

5 answers

Oczywiscie zapis do aktualnosci nie ma miejsca-przed przeczytaniem tego, ale nie widze jak to moze byc problemem.

currentPos = new Point(currentPos.x+1, currentPos.y+1); robi kilka rzeczy, w tym Zapisywanie wartości domyślnych do x i y (0), a następnie zapisywanie ich wartości początkowych w konstruktorze. Ponieważ twój obiekt nie jest bezpiecznie opublikowany, te 4 operacje zapisu mogą być dowolnie uporządkowane przez kompilator / JVM.

Więc z punktu widzenia wątku lektury, jest to legalna egzekucja, aby przeczytać {[1] } z nową wartością, ale y z domyślną wartością 0 Na przykład. Do czasu osiągnięcia instrukcji println (która przy okazji jest zsynchronizowana i dlatego ma wpływ na operacje odczytu), zmienne mają swoje początkowe wartości, a program wypisuje oczekiwane wartości.

Oznaczenie currentPos jako volatile zapewni bezpieczną publikację, ponieważ twój obiekt jest skutecznie niezmienny - jeśli w Twoim rzeczywistym przypadku użycia obiekt zostanie zmutowany po budowie, volatile gwarancje nie wystarczą i znów można było zobaczyć niespójny obiekt.

Alternatywnie możesz uczynić Point niezmiennym, co zapewni również bezpieczną publikację, nawet bez użycia volatile. Aby osiągnąć niezmienność, wystarczy zaznaczyć x i y końcowy.

Na marginesie i jak już wspomniano, synchronized(this) {} może być traktowany jako no-op przez JVM(rozumiem, że włączyłeś go, aby odtworzyć zachowanie).

 140
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
2014-06-16 07:38:23

Ponieważ currentPos jest zmieniany poza wątkiem powinien być oznaczony jako volatile:

static volatile Point currentPos = new Point(1,2);

BEZ volatile wątek nie jest gwarantowany do odczytu w aktualizacjach bieżących, które są dokonywane w głównym wątku. Tak więc nowe wartości są nadal zapisywane dla currentPos, ale thread nadal używa poprzednich wersji pamięci podręcznej ze względów wydajnościowych. Ponieważ tylko jeden wątek modyfikuje currentPos, możesz uciec bez blokad, co poprawi wydajność.

Wyniki wygląda znacznie inaczej, jeśli odczytasz wartości tylko jeden raz w wątku, aby użyć ich do porównania i późniejszego wyświetlenia. Kiedy wykonuję następujące x zawsze wyświetla się jako 1 i y waha się między 0 a jakąś dużą liczbą całkowitą. Myślę, że jego zachowanie w tym momencie jest nieco nieokreślone bez słowa kluczowego volatile i jest możliwe, że kompilacja JIT kodu przyczynia się do tego, że zachowuje się tak. Również jeśli skomentuję pusty synchronized(this) {} blok to kod działa również i podejrzewam, że to dlatego, że blokowanie powoduje wystarczające opóźnienie, że currentPos i jego pola są ponownie odczytywane, a nie używane z pamięci podręcznej.

int x = p.x + 1;
int y = p.y;

if (x != y) {
    System.out.println(x+" "+y);
    System.exit(1);
}
 29
Author: Ed Plese,
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-04-23 02:32:06

Masz zwykłą pamięć, referencję 'currentpos' oraz obiekt Point i jego pola za nim, współdzielone między 2 wątkami, bez synchronizacji. Nie ma więc zdefiniowanej kolejności pomiędzy zapisami, które przydarzają się tej pamięci w głównym wątku, a odczytami w utworzonym wątku (nazwijmy to T).

Główny wątek wykonuje następujący zapis (ignorując początkową konfigurację punktu, spowoduje to, że p.x I p.y będą miały domyślne wartości):

  • do p. x
  • do p.y
  • to currentpos

Ponieważ nie ma nic specjalnego w tych zapisach pod względem synchronizacji/barier, runtime jest wolne, aby wątek T widział, jak występują one w dowolnej kolejności (główny wątek oczywiście zawsze widzi zapis i odczyt uporządkowany zgodnie z kolejnością programu) i wystąpił w dowolnym punkcie pomiędzy odczytami w T.

Więc t robi:

  1. odczytuje bieżący zapis do p
  2. Czytaj p. x I P. y (W dowolnej kolejności)
  3. porównaj i weź branch
  4. odczyt P. x i P. y (albo kolejność) i system wywołania.Wynocha.println

Biorąc pod uwagę, że nie ma uporządkowanych relacji między zapisami w main, a odczytami w T, istnieje kilka sposobów, aby uzyskać wynik, ponieważ t może zobaczyć zapis main do currentpos przed zapisem do currentpos.y lub currentpos.x:

  1. czyta currentpos.najpierw x, zanim dojdzie do zapisu x-dostaje 0, a następnie odczytuje currentpos.y przed zapisem y-dostaje 0. / Align = "left" / / Align = "left" / stań się widoczny dla T. System.Wynocha.nazywa się println.
  2. czyta currentpos.x najpierw, po wykonaniu zapisu x, a następnie odczytuje currentpos.y przed zapisem y-otrzymuje 0. / Align = "left" / / Align = "left" / T. Kobayashi.. itd.
  3. czyta currentpos.najpierw y, zanim nastąpi zapis y( 0), następnie odczytuje currentpos.x po zapisie x, evals to true. itd.

I tak dalej... Istnieje wiele wyścigów danych proszę.

Podejrzewam, że błędnym założeniem jest myślenie, że wynik zapisu z tej linii jest widoczny we wszystkich wątkach w kolejności programowej wątku go wykonującego:

currentPos = new Point(currentPos.x+1, currentPos.y+1);

Java nie daje takiej gwarancji (byłoby to straszne dla wydajności). Coś więcej należy dodać, jeśli twój program potrzebuje gwarantowanej kolejności zapisów względem odczytów w innych wątkach. Inni sugerowali,aby Pola x, y były ostateczne lub alternatywnie tworzyły currentpos Lotny.

  • jeśli sprawisz,że pola x, y będą ostateczne, to Java gwarantuje, że zapisy ich wartości będą widoczne przed powrotem konstruktora we wszystkich wątkach. Tak więc, ponieważ przypisanie do currentpos następuje po konstruktorze, wątek T ma gwarancję, że będzie widział zapisy w odpowiedniej kolejności.
  • Jeśli zrobisz currentpos zmienny, to Java gwarantuje, że jest to punkt synchronizacji, który będzie uporządkowany w całości wrt innych punktów synchronizacji. Jak w głównej zapis do x i y musi nastąpić przed zapisem do currentpos, wtedy każdy odczyt z currentpos w innym wątku musi zobaczyć również zapis x, y, który miał miejsce wcześniej.

Użycie final ma tę zaletę, że sprawia, że pola są niezmienne, a tym samym umożliwia buforowanie wartości. Korzystanie z funkcji volatile prowadzi do synchronizacji przy każdym zapisie i odczycie bieżącego pliku, co może zaszkodzić wydajności.

Patrz Rozdział 17 specyfikacji języka Java dla gory szczegóły: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

(początkowa odpowiedź zakładała słabszy model pamięci, ponieważ nie byłem pewien, czy gwarantowany przez JLS Lotny jest wystarczający. Odpowiedź edytowana w celu odzwierciedlenia komentarza z asylias, wskazując, że model Javy jest silniejszy-zdarza się-wcześniej jest przechodni - a więc zmienny na obecnych też wystarczy).

 19
Author: paulj,
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-08-16 12:17:09

Możesz użyć obiektu do synchronizacji zapisów i odczytów. W przeciwnym razie, jak inni mówili wcześniej, zapis do currentPos nastąpi w środku dwóch odczytów p. x+1 i p. y.

new Thread() {
    void f(Point p) {
        if (p.x+1 != p.y) {
            System.out.println(p.x+" "+p.y);
            System.exit(1);
        }
    }
    @Override
    public void run() {
        while (currentPos == null);
        while (true)
            f(currentPos);
    }
}.start();
Object sem = new Object();
while (true) {
    synchronized(sem) {
        currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}
 -2
Author: Germano Fronza,
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-08-15 21:19:14

Uzyskujesz dostęp do currentPos dwa razy i nie gwarantujesz, że nie jest on aktualizowany pomiędzy tymi dwoma dostępami.

Na przykład:

  1. x = 10, y = 11
  2. wątek roboczy ocenia str. x jako 10
  3. główny wątek wykonuje aktualizację, teraz x = 11 i y = 12
  4. wątek roboczy ocenia p. y jako 12
  5. worker thread notes that 10+1 != 12, więc drukuje i wychodzi.

Zasadniczo porównujesz dwa różne Punktów.

Zauważ, że nawet tworzenie currentPos nie ochroni Cię przed tym, ponieważ są to dwa oddzielne odczyty w wątku roboczym.

Dodaj

boolean IsValid() { return x+1 == y; }

Metoda do klasy punktów. Zapewni to, że tylko jedna wartość currentPos jest używana podczas sprawdzania x+1 == y.

 -3
Author: user2686913,
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-08-15 18:10:46